From 969c24ce8352400b177d6a8a3572afc4da79750a Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 10:18:29 +0700 Subject: [PATCH 01/14] feat(skill-tree): add agentkit.skill-tree/v1 digest, archive readers and identity rules Add @skillx/skill-tree, a TypeScript implementation of the AgentKit skill tree contract shared with the AgentKit CLI: - canonical serialization and sha256 tree digest - path, NFC and case-fold collision rules on Unicode 15.0.0 tables generated from pinned inputs, never runtime Intl data - streaming tar/tar.gz and zip readers with exact entry, file and byte limits enforced while content streams (pull-driven inflate) - package identity, alias and directory alias rules, provenance trust and frontmatter exposure The AgentKit conformance vectors are vendored unedited with a provenance note and sha256 drift tests; every tree and identity vector passes. Negative tests cover links, special entries, sparse files, unsafe modes and names, case collisions, damaged archives and decompression bombs. Refs #54 --- packages/skill-tree/.gitattributes | 3 + packages/skill-tree/fixtures/PROVENANCE.md | 34 + .../fixtures/ucd/CaseFolding-15.0.0.txt | 1624 +++++++++ .../fixtures/vectors/identity-v1-vectors.json | 782 +++++ .../vectors/skill-tree-v1-vectors.json | 3091 +++++++++++++++++ packages/skill-tree/package.json | 23 + .../skill-tree/scripts/dump-go-unicode.go | 106 + .../skill-tree/scripts/gen-unicode-tables.mjs | 130 + .../skill-tree/scripts/unicode-15.0.0-go.json | 1 + packages/skill-tree/src/byte-source.ts | 172 + packages/skill-tree/src/collision.ts | 86 + packages/skill-tree/src/entry-collector.ts | 71 + packages/skill-tree/src/errors.ts | 42 + packages/skill-tree/src/exposure.ts | 76 + packages/skill-tree/src/identity.ts | 153 + packages/skill-tree/src/index.ts | 50 + packages/skill-tree/src/path-rules.ts | 155 + packages/skill-tree/src/serialize.ts | 86 + packages/skill-tree/src/tar-header.ts | 159 + packages/skill-tree/src/tar-reader.ts | 159 + packages/skill-tree/src/tree.ts | 170 + packages/skill-tree/src/unicode-15-tables.ts | 14 + packages/skill-tree/src/unicode-15.ts | 200 ++ packages/skill-tree/src/zip-reader.ts | 147 + packages/skill-tree/src/zip-records.ts | 200 ++ .../skill-tree/test/identity-vectors.test.ts | 132 + packages/skill-tree/test/path-fuzz.test.ts | 72 + packages/skill-tree/test/provenance.test.ts | 104 + packages/skill-tree/test/tar-reader.test.ts | 153 + packages/skill-tree/test/tar-writer.ts | 94 + packages/skill-tree/test/tree-vectors.test.ts | 127 + packages/skill-tree/test/vector-helpers.ts | 162 + packages/skill-tree/test/zip-reader.test.ts | 95 + packages/skill-tree/test/zip-writer.ts | 78 + packages/skill-tree/tsconfig.json | 17 + packages/skill-tree/vitest.config.ts | 8 + pnpm-lock.yaml | 82 +- vitest.config.ts | 1 + 38 files changed, 8829 insertions(+), 30 deletions(-) create mode 100644 packages/skill-tree/.gitattributes create mode 100644 packages/skill-tree/fixtures/PROVENANCE.md create mode 100644 packages/skill-tree/fixtures/ucd/CaseFolding-15.0.0.txt create mode 100644 packages/skill-tree/fixtures/vectors/identity-v1-vectors.json create mode 100644 packages/skill-tree/fixtures/vectors/skill-tree-v1-vectors.json create mode 100644 packages/skill-tree/package.json create mode 100644 packages/skill-tree/scripts/dump-go-unicode.go create mode 100644 packages/skill-tree/scripts/gen-unicode-tables.mjs create mode 100644 packages/skill-tree/scripts/unicode-15.0.0-go.json create mode 100644 packages/skill-tree/src/byte-source.ts create mode 100644 packages/skill-tree/src/collision.ts create mode 100644 packages/skill-tree/src/entry-collector.ts create mode 100644 packages/skill-tree/src/errors.ts create mode 100644 packages/skill-tree/src/exposure.ts create mode 100644 packages/skill-tree/src/identity.ts create mode 100644 packages/skill-tree/src/index.ts create mode 100644 packages/skill-tree/src/path-rules.ts create mode 100644 packages/skill-tree/src/serialize.ts create mode 100644 packages/skill-tree/src/tar-header.ts create mode 100644 packages/skill-tree/src/tar-reader.ts create mode 100644 packages/skill-tree/src/tree.ts create mode 100644 packages/skill-tree/src/unicode-15-tables.ts create mode 100644 packages/skill-tree/src/unicode-15.ts create mode 100644 packages/skill-tree/src/zip-reader.ts create mode 100644 packages/skill-tree/src/zip-records.ts create mode 100644 packages/skill-tree/test/identity-vectors.test.ts create mode 100644 packages/skill-tree/test/path-fuzz.test.ts create mode 100644 packages/skill-tree/test/provenance.test.ts create mode 100644 packages/skill-tree/test/tar-reader.test.ts create mode 100644 packages/skill-tree/test/tar-writer.ts create mode 100644 packages/skill-tree/test/tree-vectors.test.ts create mode 100644 packages/skill-tree/test/vector-helpers.ts create mode 100644 packages/skill-tree/test/zip-reader.test.ts create mode 100644 packages/skill-tree/test/zip-writer.ts create mode 100644 packages/skill-tree/tsconfig.json create mode 100644 packages/skill-tree/vitest.config.ts diff --git a/packages/skill-tree/.gitattributes b/packages/skill-tree/.gitattributes new file mode 100644 index 0000000..35fee4c --- /dev/null +++ b/packages/skill-tree/.gitattributes @@ -0,0 +1,3 @@ +# Pinned inputs are hashed byte for byte; never convert their line endings. +fixtures/** -text +scripts/unicode-15.0.0-go.json -text diff --git a/packages/skill-tree/fixtures/PROVENANCE.md b/packages/skill-tree/fixtures/PROVENANCE.md new file mode 100644 index 0000000..887cce0 --- /dev/null +++ b/packages/skill-tree/fixtures/PROVENANCE.md @@ -0,0 +1,34 @@ +# Fixture provenance + +These files are vendored unedited. Tests pin each by sha256 (`test/provenance.test.ts`), so any +edit or line-ending conversion fails the suite. A vector change is an AgentKit contract change: +update by copying the new upstream file and its new hash, never by editing a vector here. + +| File | Source | Commit | sha256 | +|---|---|---|---| +| `vectors/skill-tree-v1-vectors.json` | `bestagentkits/agentkit` `docs/conformance/skill-lifecycle/skill-tree-v1-vectors.json` | `dev` @ `479483bfe8ff022063f7bff21e75df17fabf0997` (git blob `ecb6722af896f41b176e5f5a5e1ba367c58449aa`) | `8eb46f1b8bec49cace740b75614f28f8fb4a6fc266fecfcd7e3d6892f28f3172` | +| `vectors/identity-v1-vectors.json` | `bestagentkits/agentkit` `docs/conformance/skill-lifecycle/identity-v1-vectors.json` | `dev` @ `479483bfe8ff022063f7bff21e75df17fabf0997` (git blob `d532820a500e792eef6445b0e54ba49554ab3fa6`) | `1ab9e013c229d78871abd60e3cd3bf26fcfd0e6067d98dcdd79b4f6bcc1daefb` | +| `ucd/CaseFolding-15.0.0.txt` | Unicode 15.0.0 `CaseFolding.txt`, as vendored by AgentKit at `apps/cli/internal/core/skilltree/testdata/CaseFolding-15.0.0.txt` | `840d924a7` (branch `mrgoonie/feat/skilltree-canonical-digest`) | `cdd49e55eae3bbf1f0a3f6580c974a0263cb86a6a08daa10fbf705b4808a56f7` | + +The contract: AgentKit `docs/specs/skill-lifecycle-contract.md` sections 2, 4 and 10.2, and +`docs/specs/skill-lifecycle-schemas.md` section 2.1. + +## Unicode tables + +`src/unicode-15-tables.ts` is generated by `scripts/gen-unicode-tables.mjs` from two pinned +inputs: the case folding file above, and `scripts/unicode-15.0.0-go.json`, which +`scripts/dump-go-unicode.go` writes from the Go `unicode` tables (go1.25.3) and +`golang.org/x/text/unicode/norm` (Unicode 15.0.0), the same tables the AgentKit reference +implementation validates paths with. To regenerate: + +```sh +cd /apps/cli && go run /packages/skill-tree/scripts/dump-go-unicode.go > /packages/skill-tree/scripts/unicode-15.0.0-go.json +cd /packages/skill-tree && node scripts/gen-unicode-tables.mjs +``` + +`scripts/unicode-15.0.0-go.json` is pinned at sha256 +`baa335155dcbaf0cfe8f40fcc0a98232c4134e821a08c3f198f829a51d911ad5` (generator and tests). +Tests check the fold table against the case folding file, and the category and combining-class +tables against the Go dump, for every code point. They also check +normalization against the runtime's `String.prototype.normalize` for every code point assigned +in Unicode 15.0.0 (normalization stability makes later Unicode versions agree on those). diff --git a/packages/skill-tree/fixtures/ucd/CaseFolding-15.0.0.txt b/packages/skill-tree/fixtures/ucd/CaseFolding-15.0.0.txt new file mode 100644 index 0000000..65aa0fc --- /dev/null +++ b/packages/skill-tree/fixtures/ucd/CaseFolding-15.0.0.txt @@ -0,0 +1,1624 @@ +# CaseFolding-15.0.0.txt +# Date: 2022-02-02, 23:35:35 GMT +# © 2022 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see https://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see https://www.unicode.org/reports/tr44/ +# +# Case Folding Properties +# +# This file is a supplement to the UnicodeData file. +# It provides a case folding mapping generated from the Unicode Character Database. +# If all characters are mapped according to the full mapping below, then +# case differences (according to UnicodeData.txt and SpecialCasing.txt) +# are eliminated. +# +# The data supports both implementations that require simple case foldings +# (where string lengths don't change), and implementations that allow full case folding +# (where string lengths may grow). Note that where they can be supported, the +# full case foldings are superior: for example, they allow "MASSE" and "Maße" to match. +# +# All code points not listed in this file map to themselves. +# +# NOTE: case folding does not preserve normalization formats! +# +# For information on case folding, including how to have case folding +# preserve normalization formats, see Section 3.13 Default Case Algorithms in +# The Unicode Standard. +# +# ================================================================================ +# Format +# ================================================================================ +# The entries in this file are in the following machine-readable format: +# +# ; ; ; # +# +# The status field is: +# C: common case folding, common mappings shared by both simple and full mappings. +# F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. +# S: simple case folding, mappings to single characters where different from F. +# T: special case for uppercase I and dotted uppercase I +# - For non-Turkic languages, this mapping is normally not used. +# - For Turkic languages (tr, az), this mapping can be used instead of the normal mapping for these characters. +# Note that the Turkic mappings do not maintain canonical equivalence without additional processing. +# See the discussions of case mapping in the Unicode Standard for more information. +# +# Usage: +# A. To do a simple case folding, use the mappings with status C + S. +# B. To do a full case folding, use the mappings with status C + F. +# +# The mappings with status T can be used or omitted depending on the desired case-folding +# behavior. (The default option is to exclude them.) +# +# ================================================================= + +# Property: Case_Folding + +# All code points not explicitly listed for Case_Folding +# have the value C for the status field, and the code point itself for the mapping field. + +# ================================================================= +0041; C; 0061; # LATIN CAPITAL LETTER A +0042; C; 0062; # LATIN CAPITAL LETTER B +0043; C; 0063; # LATIN CAPITAL LETTER C +0044; C; 0064; # LATIN CAPITAL LETTER D +0045; C; 0065; # LATIN CAPITAL LETTER E +0046; C; 0066; # LATIN CAPITAL LETTER F +0047; C; 0067; # LATIN CAPITAL LETTER G +0048; C; 0068; # LATIN CAPITAL LETTER H +0049; C; 0069; # LATIN CAPITAL LETTER I +0049; T; 0131; # LATIN CAPITAL LETTER I +004A; C; 006A; # LATIN CAPITAL LETTER J +004B; C; 006B; # LATIN CAPITAL LETTER K +004C; C; 006C; # LATIN CAPITAL LETTER L +004D; C; 006D; # LATIN CAPITAL LETTER M +004E; C; 006E; # LATIN CAPITAL LETTER N +004F; C; 006F; # LATIN CAPITAL LETTER O +0050; C; 0070; # LATIN CAPITAL LETTER P +0051; C; 0071; # LATIN CAPITAL LETTER Q +0052; C; 0072; # LATIN CAPITAL LETTER R +0053; C; 0073; # LATIN CAPITAL LETTER S +0054; C; 0074; # LATIN CAPITAL LETTER T +0055; C; 0075; # LATIN CAPITAL LETTER U +0056; C; 0076; # LATIN CAPITAL LETTER V +0057; C; 0077; # LATIN CAPITAL LETTER W +0058; C; 0078; # LATIN CAPITAL LETTER X +0059; C; 0079; # LATIN CAPITAL LETTER Y +005A; C; 007A; # LATIN CAPITAL LETTER Z +00B5; C; 03BC; # MICRO SIGN +00C0; C; 00E0; # LATIN CAPITAL LETTER A WITH GRAVE +00C1; C; 00E1; # LATIN CAPITAL LETTER A WITH ACUTE +00C2; C; 00E2; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX +00C3; C; 00E3; # LATIN CAPITAL LETTER A WITH TILDE +00C4; C; 00E4; # LATIN CAPITAL LETTER A WITH DIAERESIS +00C5; C; 00E5; # LATIN CAPITAL LETTER A WITH RING ABOVE +00C6; C; 00E6; # LATIN CAPITAL LETTER AE +00C7; C; 00E7; # LATIN CAPITAL LETTER C WITH CEDILLA +00C8; C; 00E8; # LATIN CAPITAL LETTER E WITH GRAVE +00C9; C; 00E9; # LATIN CAPITAL LETTER E WITH ACUTE +00CA; C; 00EA; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX +00CB; C; 00EB; # LATIN CAPITAL LETTER E WITH DIAERESIS +00CC; C; 00EC; # LATIN CAPITAL LETTER I WITH GRAVE +00CD; C; 00ED; # LATIN CAPITAL LETTER I WITH ACUTE +00CE; C; 00EE; # LATIN CAPITAL LETTER I WITH CIRCUMFLEX +00CF; C; 00EF; # LATIN CAPITAL LETTER I WITH DIAERESIS +00D0; C; 00F0; # LATIN CAPITAL LETTER ETH +00D1; C; 00F1; # LATIN CAPITAL LETTER N WITH TILDE +00D2; C; 00F2; # LATIN CAPITAL LETTER O WITH GRAVE +00D3; C; 00F3; # LATIN CAPITAL LETTER O WITH ACUTE +00D4; C; 00F4; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX +00D5; C; 00F5; # LATIN CAPITAL LETTER O WITH TILDE +00D6; C; 00F6; # LATIN CAPITAL LETTER O WITH DIAERESIS +00D8; C; 00F8; # LATIN CAPITAL LETTER O WITH STROKE +00D9; C; 00F9; # LATIN CAPITAL LETTER U WITH GRAVE +00DA; C; 00FA; # LATIN CAPITAL LETTER U WITH ACUTE +00DB; C; 00FB; # LATIN CAPITAL LETTER U WITH CIRCUMFLEX +00DC; C; 00FC; # LATIN CAPITAL LETTER U WITH DIAERESIS +00DD; C; 00FD; # LATIN CAPITAL LETTER Y WITH ACUTE +00DE; C; 00FE; # LATIN CAPITAL LETTER THORN +00DF; F; 0073 0073; # LATIN SMALL LETTER SHARP S +0100; C; 0101; # LATIN CAPITAL LETTER A WITH MACRON +0102; C; 0103; # LATIN CAPITAL LETTER A WITH BREVE +0104; C; 0105; # LATIN CAPITAL LETTER A WITH OGONEK +0106; C; 0107; # LATIN CAPITAL LETTER C WITH ACUTE +0108; C; 0109; # LATIN CAPITAL LETTER C WITH CIRCUMFLEX +010A; C; 010B; # LATIN CAPITAL LETTER C WITH DOT ABOVE +010C; C; 010D; # LATIN CAPITAL LETTER C WITH CARON +010E; C; 010F; # LATIN CAPITAL LETTER D WITH CARON +0110; C; 0111; # LATIN CAPITAL LETTER D WITH STROKE +0112; C; 0113; # LATIN CAPITAL LETTER E WITH MACRON +0114; C; 0115; # LATIN CAPITAL LETTER E WITH BREVE +0116; C; 0117; # LATIN CAPITAL LETTER E WITH DOT ABOVE +0118; C; 0119; # LATIN CAPITAL LETTER E WITH OGONEK +011A; C; 011B; # LATIN CAPITAL LETTER E WITH CARON +011C; C; 011D; # LATIN CAPITAL LETTER G WITH CIRCUMFLEX +011E; C; 011F; # LATIN CAPITAL LETTER G WITH BREVE +0120; C; 0121; # LATIN CAPITAL LETTER G WITH DOT ABOVE +0122; C; 0123; # LATIN CAPITAL LETTER G WITH CEDILLA +0124; C; 0125; # LATIN CAPITAL LETTER H WITH CIRCUMFLEX +0126; C; 0127; # LATIN CAPITAL LETTER H WITH STROKE +0128; C; 0129; # LATIN CAPITAL LETTER I WITH TILDE +012A; C; 012B; # LATIN CAPITAL LETTER I WITH MACRON +012C; C; 012D; # LATIN CAPITAL LETTER I WITH BREVE +012E; C; 012F; # LATIN CAPITAL LETTER I WITH OGONEK +0130; F; 0069 0307; # LATIN CAPITAL LETTER I WITH DOT ABOVE +0130; T; 0069; # LATIN CAPITAL LETTER I WITH DOT ABOVE +0132; C; 0133; # LATIN CAPITAL LIGATURE IJ +0134; C; 0135; # LATIN CAPITAL LETTER J WITH CIRCUMFLEX +0136; C; 0137; # LATIN CAPITAL LETTER K WITH CEDILLA +0139; C; 013A; # LATIN CAPITAL LETTER L WITH ACUTE +013B; C; 013C; # LATIN CAPITAL LETTER L WITH CEDILLA +013D; C; 013E; # LATIN CAPITAL LETTER L WITH CARON +013F; C; 0140; # LATIN CAPITAL LETTER L WITH MIDDLE DOT +0141; C; 0142; # LATIN CAPITAL LETTER L WITH STROKE +0143; C; 0144; # LATIN CAPITAL LETTER N WITH ACUTE +0145; C; 0146; # LATIN CAPITAL LETTER N WITH CEDILLA +0147; C; 0148; # LATIN CAPITAL LETTER N WITH CARON +0149; F; 02BC 006E; # LATIN SMALL LETTER N PRECEDED BY APOSTROPHE +014A; C; 014B; # LATIN CAPITAL LETTER ENG +014C; C; 014D; # LATIN CAPITAL LETTER O WITH MACRON +014E; C; 014F; # LATIN CAPITAL LETTER O WITH BREVE +0150; C; 0151; # LATIN CAPITAL LETTER O WITH DOUBLE ACUTE +0152; C; 0153; # LATIN CAPITAL LIGATURE OE +0154; C; 0155; # LATIN CAPITAL LETTER R WITH ACUTE +0156; C; 0157; # LATIN CAPITAL LETTER R WITH CEDILLA +0158; C; 0159; # LATIN CAPITAL LETTER R WITH CARON +015A; C; 015B; # LATIN CAPITAL LETTER S WITH ACUTE +015C; C; 015D; # LATIN CAPITAL LETTER S WITH CIRCUMFLEX +015E; C; 015F; # LATIN CAPITAL LETTER S WITH CEDILLA +0160; C; 0161; # LATIN CAPITAL LETTER S WITH CARON +0162; C; 0163; # LATIN CAPITAL LETTER T WITH CEDILLA +0164; C; 0165; # LATIN CAPITAL LETTER T WITH CARON +0166; C; 0167; # LATIN CAPITAL LETTER T WITH STROKE +0168; C; 0169; # LATIN CAPITAL LETTER U WITH TILDE +016A; C; 016B; # LATIN CAPITAL LETTER U WITH MACRON +016C; C; 016D; # LATIN CAPITAL LETTER U WITH BREVE +016E; C; 016F; # LATIN CAPITAL LETTER U WITH RING ABOVE +0170; C; 0171; # LATIN CAPITAL LETTER U WITH DOUBLE ACUTE +0172; C; 0173; # LATIN CAPITAL LETTER U WITH OGONEK +0174; C; 0175; # LATIN CAPITAL LETTER W WITH CIRCUMFLEX +0176; C; 0177; # LATIN CAPITAL LETTER Y WITH CIRCUMFLEX +0178; C; 00FF; # LATIN CAPITAL LETTER Y WITH DIAERESIS +0179; C; 017A; # LATIN CAPITAL LETTER Z WITH ACUTE +017B; C; 017C; # LATIN CAPITAL LETTER Z WITH DOT ABOVE +017D; C; 017E; # LATIN CAPITAL LETTER Z WITH CARON +017F; C; 0073; # LATIN SMALL LETTER LONG S +0181; C; 0253; # LATIN CAPITAL LETTER B WITH HOOK +0182; C; 0183; # LATIN CAPITAL LETTER B WITH TOPBAR +0184; C; 0185; # LATIN CAPITAL LETTER TONE SIX +0186; C; 0254; # LATIN CAPITAL LETTER OPEN O +0187; C; 0188; # LATIN CAPITAL LETTER C WITH HOOK +0189; C; 0256; # LATIN CAPITAL LETTER AFRICAN D +018A; C; 0257; # LATIN CAPITAL LETTER D WITH HOOK +018B; C; 018C; # LATIN CAPITAL LETTER D WITH TOPBAR +018E; C; 01DD; # LATIN CAPITAL LETTER REVERSED E +018F; C; 0259; # LATIN CAPITAL LETTER SCHWA +0190; C; 025B; # LATIN CAPITAL LETTER OPEN E +0191; C; 0192; # LATIN CAPITAL LETTER F WITH HOOK +0193; C; 0260; # LATIN CAPITAL LETTER G WITH HOOK +0194; C; 0263; # LATIN CAPITAL LETTER GAMMA +0196; C; 0269; # LATIN CAPITAL LETTER IOTA +0197; C; 0268; # LATIN CAPITAL LETTER I WITH STROKE +0198; C; 0199; # LATIN CAPITAL LETTER K WITH HOOK +019C; C; 026F; # LATIN CAPITAL LETTER TURNED M +019D; C; 0272; # LATIN CAPITAL LETTER N WITH LEFT HOOK +019F; C; 0275; # LATIN CAPITAL LETTER O WITH MIDDLE TILDE +01A0; C; 01A1; # LATIN CAPITAL LETTER O WITH HORN +01A2; C; 01A3; # LATIN CAPITAL LETTER OI +01A4; C; 01A5; # LATIN CAPITAL LETTER P WITH HOOK +01A6; C; 0280; # LATIN LETTER YR +01A7; C; 01A8; # LATIN CAPITAL LETTER TONE TWO +01A9; C; 0283; # LATIN CAPITAL LETTER ESH +01AC; C; 01AD; # LATIN CAPITAL LETTER T WITH HOOK +01AE; C; 0288; # LATIN CAPITAL LETTER T WITH RETROFLEX HOOK +01AF; C; 01B0; # LATIN CAPITAL LETTER U WITH HORN +01B1; C; 028A; # LATIN CAPITAL LETTER UPSILON +01B2; C; 028B; # LATIN CAPITAL LETTER V WITH HOOK +01B3; C; 01B4; # LATIN CAPITAL LETTER Y WITH HOOK +01B5; C; 01B6; # LATIN CAPITAL LETTER Z WITH STROKE +01B7; C; 0292; # LATIN CAPITAL LETTER EZH +01B8; C; 01B9; # LATIN CAPITAL LETTER EZH REVERSED +01BC; C; 01BD; # LATIN CAPITAL LETTER TONE FIVE +01C4; C; 01C6; # LATIN CAPITAL LETTER DZ WITH CARON +01C5; C; 01C6; # LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON +01C7; C; 01C9; # LATIN CAPITAL LETTER LJ +01C8; C; 01C9; # LATIN CAPITAL LETTER L WITH SMALL LETTER J +01CA; C; 01CC; # LATIN CAPITAL LETTER NJ +01CB; C; 01CC; # LATIN CAPITAL LETTER N WITH SMALL LETTER J +01CD; C; 01CE; # LATIN CAPITAL LETTER A WITH CARON +01CF; C; 01D0; # LATIN CAPITAL LETTER I WITH CARON +01D1; C; 01D2; # LATIN CAPITAL LETTER O WITH CARON +01D3; C; 01D4; # LATIN CAPITAL LETTER U WITH CARON +01D5; C; 01D6; # LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON +01D7; C; 01D8; # LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE +01D9; C; 01DA; # LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON +01DB; C; 01DC; # LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE +01DE; C; 01DF; # LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON +01E0; C; 01E1; # LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON +01E2; C; 01E3; # LATIN CAPITAL LETTER AE WITH MACRON +01E4; C; 01E5; # LATIN CAPITAL LETTER G WITH STROKE +01E6; C; 01E7; # LATIN CAPITAL LETTER G WITH CARON +01E8; C; 01E9; # LATIN CAPITAL LETTER K WITH CARON +01EA; C; 01EB; # LATIN CAPITAL LETTER O WITH OGONEK +01EC; C; 01ED; # LATIN CAPITAL LETTER O WITH OGONEK AND MACRON +01EE; C; 01EF; # LATIN CAPITAL LETTER EZH WITH CARON +01F0; F; 006A 030C; # LATIN SMALL LETTER J WITH CARON +01F1; C; 01F3; # LATIN CAPITAL LETTER DZ +01F2; C; 01F3; # LATIN CAPITAL LETTER D WITH SMALL LETTER Z +01F4; C; 01F5; # LATIN CAPITAL LETTER G WITH ACUTE +01F6; C; 0195; # LATIN CAPITAL LETTER HWAIR +01F7; C; 01BF; # LATIN CAPITAL LETTER WYNN +01F8; C; 01F9; # LATIN CAPITAL LETTER N WITH GRAVE +01FA; C; 01FB; # LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE +01FC; C; 01FD; # LATIN CAPITAL LETTER AE WITH ACUTE +01FE; C; 01FF; # LATIN CAPITAL LETTER O WITH STROKE AND ACUTE +0200; C; 0201; # LATIN CAPITAL LETTER A WITH DOUBLE GRAVE +0202; C; 0203; # LATIN CAPITAL LETTER A WITH INVERTED BREVE +0204; C; 0205; # LATIN CAPITAL LETTER E WITH DOUBLE GRAVE +0206; C; 0207; # LATIN CAPITAL LETTER E WITH INVERTED BREVE +0208; C; 0209; # LATIN CAPITAL LETTER I WITH DOUBLE GRAVE +020A; C; 020B; # LATIN CAPITAL LETTER I WITH INVERTED BREVE +020C; C; 020D; # LATIN CAPITAL LETTER O WITH DOUBLE GRAVE +020E; C; 020F; # LATIN CAPITAL LETTER O WITH INVERTED BREVE +0210; C; 0211; # LATIN CAPITAL LETTER R WITH DOUBLE GRAVE +0212; C; 0213; # LATIN CAPITAL LETTER R WITH INVERTED BREVE +0214; C; 0215; # LATIN CAPITAL LETTER U WITH DOUBLE GRAVE +0216; C; 0217; # LATIN CAPITAL LETTER U WITH INVERTED BREVE +0218; C; 0219; # LATIN CAPITAL LETTER S WITH COMMA BELOW +021A; C; 021B; # LATIN CAPITAL LETTER T WITH COMMA BELOW +021C; C; 021D; # LATIN CAPITAL LETTER YOGH +021E; C; 021F; # LATIN CAPITAL LETTER H WITH CARON +0220; C; 019E; # LATIN CAPITAL LETTER N WITH LONG RIGHT LEG +0222; C; 0223; # LATIN CAPITAL LETTER OU +0224; C; 0225; # LATIN CAPITAL LETTER Z WITH HOOK +0226; C; 0227; # LATIN CAPITAL LETTER A WITH DOT ABOVE +0228; C; 0229; # LATIN CAPITAL LETTER E WITH CEDILLA +022A; C; 022B; # LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON +022C; C; 022D; # LATIN CAPITAL LETTER O WITH TILDE AND MACRON +022E; C; 022F; # LATIN CAPITAL LETTER O WITH DOT ABOVE +0230; C; 0231; # LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON +0232; C; 0233; # LATIN CAPITAL LETTER Y WITH MACRON +023A; C; 2C65; # LATIN CAPITAL LETTER A WITH STROKE +023B; C; 023C; # LATIN CAPITAL LETTER C WITH STROKE +023D; C; 019A; # LATIN CAPITAL LETTER L WITH BAR +023E; C; 2C66; # LATIN CAPITAL LETTER T WITH DIAGONAL STROKE +0241; C; 0242; # LATIN CAPITAL LETTER GLOTTAL STOP +0243; C; 0180; # LATIN CAPITAL LETTER B WITH STROKE +0244; C; 0289; # LATIN CAPITAL LETTER U BAR +0245; C; 028C; # LATIN CAPITAL LETTER TURNED V +0246; C; 0247; # LATIN CAPITAL LETTER E WITH STROKE +0248; C; 0249; # LATIN CAPITAL LETTER J WITH STROKE +024A; C; 024B; # LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL +024C; C; 024D; # LATIN CAPITAL LETTER R WITH STROKE +024E; C; 024F; # LATIN CAPITAL LETTER Y WITH STROKE +0345; C; 03B9; # COMBINING GREEK YPOGEGRAMMENI +0370; C; 0371; # GREEK CAPITAL LETTER HETA +0372; C; 0373; # GREEK CAPITAL LETTER ARCHAIC SAMPI +0376; C; 0377; # GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA +037F; C; 03F3; # GREEK CAPITAL LETTER YOT +0386; C; 03AC; # GREEK CAPITAL LETTER ALPHA WITH TONOS +0388; C; 03AD; # GREEK CAPITAL LETTER EPSILON WITH TONOS +0389; C; 03AE; # GREEK CAPITAL LETTER ETA WITH TONOS +038A; C; 03AF; # GREEK CAPITAL LETTER IOTA WITH TONOS +038C; C; 03CC; # GREEK CAPITAL LETTER OMICRON WITH TONOS +038E; C; 03CD; # GREEK CAPITAL LETTER UPSILON WITH TONOS +038F; C; 03CE; # GREEK CAPITAL LETTER OMEGA WITH TONOS +0390; F; 03B9 0308 0301; # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS +0391; C; 03B1; # GREEK CAPITAL LETTER ALPHA +0392; C; 03B2; # GREEK CAPITAL LETTER BETA +0393; C; 03B3; # GREEK CAPITAL LETTER GAMMA +0394; C; 03B4; # GREEK CAPITAL LETTER DELTA +0395; C; 03B5; # GREEK CAPITAL LETTER EPSILON +0396; C; 03B6; # GREEK CAPITAL LETTER ZETA +0397; C; 03B7; # GREEK CAPITAL LETTER ETA +0398; C; 03B8; # GREEK CAPITAL LETTER THETA +0399; C; 03B9; # GREEK CAPITAL LETTER IOTA +039A; C; 03BA; # GREEK CAPITAL LETTER KAPPA +039B; C; 03BB; # GREEK CAPITAL LETTER LAMDA +039C; C; 03BC; # GREEK CAPITAL LETTER MU +039D; C; 03BD; # GREEK CAPITAL LETTER NU +039E; C; 03BE; # GREEK CAPITAL LETTER XI +039F; C; 03BF; # GREEK CAPITAL LETTER OMICRON +03A0; C; 03C0; # GREEK CAPITAL LETTER PI +03A1; C; 03C1; # GREEK CAPITAL LETTER RHO +03A3; C; 03C3; # GREEK CAPITAL LETTER SIGMA +03A4; C; 03C4; # GREEK CAPITAL LETTER TAU +03A5; C; 03C5; # GREEK CAPITAL LETTER UPSILON +03A6; C; 03C6; # GREEK CAPITAL LETTER PHI +03A7; C; 03C7; # GREEK CAPITAL LETTER CHI +03A8; C; 03C8; # GREEK CAPITAL LETTER PSI +03A9; C; 03C9; # GREEK CAPITAL LETTER OMEGA +03AA; C; 03CA; # GREEK CAPITAL LETTER IOTA WITH DIALYTIKA +03AB; C; 03CB; # GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA +03B0; F; 03C5 0308 0301; # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS +03C2; C; 03C3; # GREEK SMALL LETTER FINAL SIGMA +03CF; C; 03D7; # GREEK CAPITAL KAI SYMBOL +03D0; C; 03B2; # GREEK BETA SYMBOL +03D1; C; 03B8; # GREEK THETA SYMBOL +03D5; C; 03C6; # GREEK PHI SYMBOL +03D6; C; 03C0; # GREEK PI SYMBOL +03D8; C; 03D9; # GREEK LETTER ARCHAIC KOPPA +03DA; C; 03DB; # GREEK LETTER STIGMA +03DC; C; 03DD; # GREEK LETTER DIGAMMA +03DE; C; 03DF; # GREEK LETTER KOPPA +03E0; C; 03E1; # GREEK LETTER SAMPI +03E2; C; 03E3; # COPTIC CAPITAL LETTER SHEI +03E4; C; 03E5; # COPTIC CAPITAL LETTER FEI +03E6; C; 03E7; # COPTIC CAPITAL LETTER KHEI +03E8; C; 03E9; # COPTIC CAPITAL LETTER HORI +03EA; C; 03EB; # COPTIC CAPITAL LETTER GANGIA +03EC; C; 03ED; # COPTIC CAPITAL LETTER SHIMA +03EE; C; 03EF; # COPTIC CAPITAL LETTER DEI +03F0; C; 03BA; # GREEK KAPPA SYMBOL +03F1; C; 03C1; # GREEK RHO SYMBOL +03F4; C; 03B8; # GREEK CAPITAL THETA SYMBOL +03F5; C; 03B5; # GREEK LUNATE EPSILON SYMBOL +03F7; C; 03F8; # GREEK CAPITAL LETTER SHO +03F9; C; 03F2; # GREEK CAPITAL LUNATE SIGMA SYMBOL +03FA; C; 03FB; # GREEK CAPITAL LETTER SAN +03FD; C; 037B; # GREEK CAPITAL REVERSED LUNATE SIGMA SYMBOL +03FE; C; 037C; # GREEK CAPITAL DOTTED LUNATE SIGMA SYMBOL +03FF; C; 037D; # GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL +0400; C; 0450; # CYRILLIC CAPITAL LETTER IE WITH GRAVE +0401; C; 0451; # CYRILLIC CAPITAL LETTER IO +0402; C; 0452; # CYRILLIC CAPITAL LETTER DJE +0403; C; 0453; # CYRILLIC CAPITAL LETTER GJE +0404; C; 0454; # CYRILLIC CAPITAL LETTER UKRAINIAN IE +0405; C; 0455; # CYRILLIC CAPITAL LETTER DZE +0406; C; 0456; # CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I +0407; C; 0457; # CYRILLIC CAPITAL LETTER YI +0408; C; 0458; # CYRILLIC CAPITAL LETTER JE +0409; C; 0459; # CYRILLIC CAPITAL LETTER LJE +040A; C; 045A; # CYRILLIC CAPITAL LETTER NJE +040B; C; 045B; # CYRILLIC CAPITAL LETTER TSHE +040C; C; 045C; # CYRILLIC CAPITAL LETTER KJE +040D; C; 045D; # CYRILLIC CAPITAL LETTER I WITH GRAVE +040E; C; 045E; # CYRILLIC CAPITAL LETTER SHORT U +040F; C; 045F; # CYRILLIC CAPITAL LETTER DZHE +0410; C; 0430; # CYRILLIC CAPITAL LETTER A +0411; C; 0431; # CYRILLIC CAPITAL LETTER BE +0412; C; 0432; # CYRILLIC CAPITAL LETTER VE +0413; C; 0433; # CYRILLIC CAPITAL LETTER GHE +0414; C; 0434; # CYRILLIC CAPITAL LETTER DE +0415; C; 0435; # CYRILLIC CAPITAL LETTER IE +0416; C; 0436; # CYRILLIC CAPITAL LETTER ZHE +0417; C; 0437; # CYRILLIC CAPITAL LETTER ZE +0418; C; 0438; # CYRILLIC CAPITAL LETTER I +0419; C; 0439; # CYRILLIC CAPITAL LETTER SHORT I +041A; C; 043A; # CYRILLIC CAPITAL LETTER KA +041B; C; 043B; # CYRILLIC CAPITAL LETTER EL +041C; C; 043C; # CYRILLIC CAPITAL LETTER EM +041D; C; 043D; # CYRILLIC CAPITAL LETTER EN +041E; C; 043E; # CYRILLIC CAPITAL LETTER O +041F; C; 043F; # CYRILLIC CAPITAL LETTER PE +0420; C; 0440; # CYRILLIC CAPITAL LETTER ER +0421; C; 0441; # CYRILLIC CAPITAL LETTER ES +0422; C; 0442; # CYRILLIC CAPITAL LETTER TE +0423; C; 0443; # CYRILLIC CAPITAL LETTER U +0424; C; 0444; # CYRILLIC CAPITAL LETTER EF +0425; C; 0445; # CYRILLIC CAPITAL LETTER HA +0426; C; 0446; # CYRILLIC CAPITAL LETTER TSE +0427; C; 0447; # CYRILLIC CAPITAL LETTER CHE +0428; C; 0448; # CYRILLIC CAPITAL LETTER SHA +0429; C; 0449; # CYRILLIC CAPITAL LETTER SHCHA +042A; C; 044A; # CYRILLIC CAPITAL LETTER HARD SIGN +042B; C; 044B; # CYRILLIC CAPITAL LETTER YERU +042C; C; 044C; # CYRILLIC CAPITAL LETTER SOFT SIGN +042D; C; 044D; # CYRILLIC CAPITAL LETTER E +042E; C; 044E; # CYRILLIC CAPITAL LETTER YU +042F; C; 044F; # CYRILLIC CAPITAL LETTER YA +0460; C; 0461; # CYRILLIC CAPITAL LETTER OMEGA +0462; C; 0463; # CYRILLIC CAPITAL LETTER YAT +0464; C; 0465; # CYRILLIC CAPITAL LETTER IOTIFIED E +0466; C; 0467; # CYRILLIC CAPITAL LETTER LITTLE YUS +0468; C; 0469; # CYRILLIC CAPITAL LETTER IOTIFIED LITTLE YUS +046A; C; 046B; # CYRILLIC CAPITAL LETTER BIG YUS +046C; C; 046D; # CYRILLIC CAPITAL LETTER IOTIFIED BIG YUS +046E; C; 046F; # CYRILLIC CAPITAL LETTER KSI +0470; C; 0471; # CYRILLIC CAPITAL LETTER PSI +0472; C; 0473; # CYRILLIC CAPITAL LETTER FITA +0474; C; 0475; # CYRILLIC CAPITAL LETTER IZHITSA +0476; C; 0477; # CYRILLIC CAPITAL LETTER IZHITSA WITH DOUBLE GRAVE ACCENT +0478; C; 0479; # CYRILLIC CAPITAL LETTER UK +047A; C; 047B; # CYRILLIC CAPITAL LETTER ROUND OMEGA +047C; C; 047D; # CYRILLIC CAPITAL LETTER OMEGA WITH TITLO +047E; C; 047F; # CYRILLIC CAPITAL LETTER OT +0480; C; 0481; # CYRILLIC CAPITAL LETTER KOPPA +048A; C; 048B; # CYRILLIC CAPITAL LETTER SHORT I WITH TAIL +048C; C; 048D; # CYRILLIC CAPITAL LETTER SEMISOFT SIGN +048E; C; 048F; # CYRILLIC CAPITAL LETTER ER WITH TICK +0490; C; 0491; # CYRILLIC CAPITAL LETTER GHE WITH UPTURN +0492; C; 0493; # CYRILLIC CAPITAL LETTER GHE WITH STROKE +0494; C; 0495; # CYRILLIC CAPITAL LETTER GHE WITH MIDDLE HOOK +0496; C; 0497; # CYRILLIC CAPITAL LETTER ZHE WITH DESCENDER +0498; C; 0499; # CYRILLIC CAPITAL LETTER ZE WITH DESCENDER +049A; C; 049B; # CYRILLIC CAPITAL LETTER KA WITH DESCENDER +049C; C; 049D; # CYRILLIC CAPITAL LETTER KA WITH VERTICAL STROKE +049E; C; 049F; # CYRILLIC CAPITAL LETTER KA WITH STROKE +04A0; C; 04A1; # CYRILLIC CAPITAL LETTER BASHKIR KA +04A2; C; 04A3; # CYRILLIC CAPITAL LETTER EN WITH DESCENDER +04A4; C; 04A5; # CYRILLIC CAPITAL LIGATURE EN GHE +04A6; C; 04A7; # CYRILLIC CAPITAL LETTER PE WITH MIDDLE HOOK +04A8; C; 04A9; # CYRILLIC CAPITAL LETTER ABKHASIAN HA +04AA; C; 04AB; # CYRILLIC CAPITAL LETTER ES WITH DESCENDER +04AC; C; 04AD; # CYRILLIC CAPITAL LETTER TE WITH DESCENDER +04AE; C; 04AF; # CYRILLIC CAPITAL LETTER STRAIGHT U +04B0; C; 04B1; # CYRILLIC CAPITAL LETTER STRAIGHT U WITH STROKE +04B2; C; 04B3; # CYRILLIC CAPITAL LETTER HA WITH DESCENDER +04B4; C; 04B5; # CYRILLIC CAPITAL LIGATURE TE TSE +04B6; C; 04B7; # CYRILLIC CAPITAL LETTER CHE WITH DESCENDER +04B8; C; 04B9; # CYRILLIC CAPITAL LETTER CHE WITH VERTICAL STROKE +04BA; C; 04BB; # CYRILLIC CAPITAL LETTER SHHA +04BC; C; 04BD; # CYRILLIC CAPITAL LETTER ABKHASIAN CHE +04BE; C; 04BF; # CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER +04C0; C; 04CF; # CYRILLIC LETTER PALOCHKA +04C1; C; 04C2; # CYRILLIC CAPITAL LETTER ZHE WITH BREVE +04C3; C; 04C4; # CYRILLIC CAPITAL LETTER KA WITH HOOK +04C5; C; 04C6; # CYRILLIC CAPITAL LETTER EL WITH TAIL +04C7; C; 04C8; # CYRILLIC CAPITAL LETTER EN WITH HOOK +04C9; C; 04CA; # CYRILLIC CAPITAL LETTER EN WITH TAIL +04CB; C; 04CC; # CYRILLIC CAPITAL LETTER KHAKASSIAN CHE +04CD; C; 04CE; # CYRILLIC CAPITAL LETTER EM WITH TAIL +04D0; C; 04D1; # CYRILLIC CAPITAL LETTER A WITH BREVE +04D2; C; 04D3; # CYRILLIC CAPITAL LETTER A WITH DIAERESIS +04D4; C; 04D5; # CYRILLIC CAPITAL LIGATURE A IE +04D6; C; 04D7; # CYRILLIC CAPITAL LETTER IE WITH BREVE +04D8; C; 04D9; # CYRILLIC CAPITAL LETTER SCHWA +04DA; C; 04DB; # CYRILLIC CAPITAL LETTER SCHWA WITH DIAERESIS +04DC; C; 04DD; # CYRILLIC CAPITAL LETTER ZHE WITH DIAERESIS +04DE; C; 04DF; # CYRILLIC CAPITAL LETTER ZE WITH DIAERESIS +04E0; C; 04E1; # CYRILLIC CAPITAL LETTER ABKHASIAN DZE +04E2; C; 04E3; # CYRILLIC CAPITAL LETTER I WITH MACRON +04E4; C; 04E5; # CYRILLIC CAPITAL LETTER I WITH DIAERESIS +04E6; C; 04E7; # CYRILLIC CAPITAL LETTER O WITH DIAERESIS +04E8; C; 04E9; # CYRILLIC CAPITAL LETTER BARRED O +04EA; C; 04EB; # CYRILLIC CAPITAL LETTER BARRED O WITH DIAERESIS +04EC; C; 04ED; # CYRILLIC CAPITAL LETTER E WITH DIAERESIS +04EE; C; 04EF; # CYRILLIC CAPITAL LETTER U WITH MACRON +04F0; C; 04F1; # CYRILLIC CAPITAL LETTER U WITH DIAERESIS +04F2; C; 04F3; # CYRILLIC CAPITAL LETTER U WITH DOUBLE ACUTE +04F4; C; 04F5; # CYRILLIC CAPITAL LETTER CHE WITH DIAERESIS +04F6; C; 04F7; # CYRILLIC CAPITAL LETTER GHE WITH DESCENDER +04F8; C; 04F9; # CYRILLIC CAPITAL LETTER YERU WITH DIAERESIS +04FA; C; 04FB; # CYRILLIC CAPITAL LETTER GHE WITH STROKE AND HOOK +04FC; C; 04FD; # CYRILLIC CAPITAL LETTER HA WITH HOOK +04FE; C; 04FF; # CYRILLIC CAPITAL LETTER HA WITH STROKE +0500; C; 0501; # CYRILLIC CAPITAL LETTER KOMI DE +0502; C; 0503; # CYRILLIC CAPITAL LETTER KOMI DJE +0504; C; 0505; # CYRILLIC CAPITAL LETTER KOMI ZJE +0506; C; 0507; # CYRILLIC CAPITAL LETTER KOMI DZJE +0508; C; 0509; # CYRILLIC CAPITAL LETTER KOMI LJE +050A; C; 050B; # CYRILLIC CAPITAL LETTER KOMI NJE +050C; C; 050D; # CYRILLIC CAPITAL LETTER KOMI SJE +050E; C; 050F; # CYRILLIC CAPITAL LETTER KOMI TJE +0510; C; 0511; # CYRILLIC CAPITAL LETTER REVERSED ZE +0512; C; 0513; # CYRILLIC CAPITAL LETTER EL WITH HOOK +0514; C; 0515; # CYRILLIC CAPITAL LETTER LHA +0516; C; 0517; # CYRILLIC CAPITAL LETTER RHA +0518; C; 0519; # CYRILLIC CAPITAL LETTER YAE +051A; C; 051B; # CYRILLIC CAPITAL LETTER QA +051C; C; 051D; # CYRILLIC CAPITAL LETTER WE +051E; C; 051F; # CYRILLIC CAPITAL LETTER ALEUT KA +0520; C; 0521; # CYRILLIC CAPITAL LETTER EL WITH MIDDLE HOOK +0522; C; 0523; # CYRILLIC CAPITAL LETTER EN WITH MIDDLE HOOK +0524; C; 0525; # CYRILLIC CAPITAL LETTER PE WITH DESCENDER +0526; C; 0527; # CYRILLIC CAPITAL LETTER SHHA WITH DESCENDER +0528; C; 0529; # CYRILLIC CAPITAL LETTER EN WITH LEFT HOOK +052A; C; 052B; # CYRILLIC CAPITAL LETTER DZZHE +052C; C; 052D; # CYRILLIC CAPITAL LETTER DCHE +052E; C; 052F; # CYRILLIC CAPITAL LETTER EL WITH DESCENDER +0531; C; 0561; # ARMENIAN CAPITAL LETTER AYB +0532; C; 0562; # ARMENIAN CAPITAL LETTER BEN +0533; C; 0563; # ARMENIAN CAPITAL LETTER GIM +0534; C; 0564; # ARMENIAN CAPITAL LETTER DA +0535; C; 0565; # ARMENIAN CAPITAL LETTER ECH +0536; C; 0566; # ARMENIAN CAPITAL LETTER ZA +0537; C; 0567; # ARMENIAN CAPITAL LETTER EH +0538; C; 0568; # ARMENIAN CAPITAL LETTER ET +0539; C; 0569; # ARMENIAN CAPITAL LETTER TO +053A; C; 056A; # ARMENIAN CAPITAL LETTER ZHE +053B; C; 056B; # ARMENIAN CAPITAL LETTER INI +053C; C; 056C; # ARMENIAN CAPITAL LETTER LIWN +053D; C; 056D; # ARMENIAN CAPITAL LETTER XEH +053E; C; 056E; # ARMENIAN CAPITAL LETTER CA +053F; C; 056F; # ARMENIAN CAPITAL LETTER KEN +0540; C; 0570; # ARMENIAN CAPITAL LETTER HO +0541; C; 0571; # ARMENIAN CAPITAL LETTER JA +0542; C; 0572; # ARMENIAN CAPITAL LETTER GHAD +0543; C; 0573; # ARMENIAN CAPITAL LETTER CHEH +0544; C; 0574; # ARMENIAN CAPITAL LETTER MEN +0545; C; 0575; # ARMENIAN CAPITAL LETTER YI +0546; C; 0576; # ARMENIAN CAPITAL LETTER NOW +0547; C; 0577; # ARMENIAN CAPITAL LETTER SHA +0548; C; 0578; # ARMENIAN CAPITAL LETTER VO +0549; C; 0579; # ARMENIAN CAPITAL LETTER CHA +054A; C; 057A; # ARMENIAN CAPITAL LETTER PEH +054B; C; 057B; # ARMENIAN CAPITAL LETTER JHEH +054C; C; 057C; # ARMENIAN CAPITAL LETTER RA +054D; C; 057D; # ARMENIAN CAPITAL LETTER SEH +054E; C; 057E; # ARMENIAN CAPITAL LETTER VEW +054F; C; 057F; # ARMENIAN CAPITAL LETTER TIWN +0550; C; 0580; # ARMENIAN CAPITAL LETTER REH +0551; C; 0581; # ARMENIAN CAPITAL LETTER CO +0552; C; 0582; # ARMENIAN CAPITAL LETTER YIWN +0553; C; 0583; # ARMENIAN CAPITAL LETTER PIWR +0554; C; 0584; # ARMENIAN CAPITAL LETTER KEH +0555; C; 0585; # ARMENIAN CAPITAL LETTER OH +0556; C; 0586; # ARMENIAN CAPITAL LETTER FEH +0587; F; 0565 0582; # ARMENIAN SMALL LIGATURE ECH YIWN +10A0; C; 2D00; # GEORGIAN CAPITAL LETTER AN +10A1; C; 2D01; # GEORGIAN CAPITAL LETTER BAN +10A2; C; 2D02; # GEORGIAN CAPITAL LETTER GAN +10A3; C; 2D03; # GEORGIAN CAPITAL LETTER DON +10A4; C; 2D04; # GEORGIAN CAPITAL LETTER EN +10A5; C; 2D05; # GEORGIAN CAPITAL LETTER VIN +10A6; C; 2D06; # GEORGIAN CAPITAL LETTER ZEN +10A7; C; 2D07; # GEORGIAN CAPITAL LETTER TAN +10A8; C; 2D08; # GEORGIAN CAPITAL LETTER IN +10A9; C; 2D09; # GEORGIAN CAPITAL LETTER KAN +10AA; C; 2D0A; # GEORGIAN CAPITAL LETTER LAS +10AB; C; 2D0B; # GEORGIAN CAPITAL LETTER MAN +10AC; C; 2D0C; # GEORGIAN CAPITAL LETTER NAR +10AD; C; 2D0D; # GEORGIAN CAPITAL LETTER ON +10AE; C; 2D0E; # GEORGIAN CAPITAL LETTER PAR +10AF; C; 2D0F; # GEORGIAN CAPITAL LETTER ZHAR +10B0; C; 2D10; # GEORGIAN CAPITAL LETTER RAE +10B1; C; 2D11; # GEORGIAN CAPITAL LETTER SAN +10B2; C; 2D12; # GEORGIAN CAPITAL LETTER TAR +10B3; C; 2D13; # GEORGIAN CAPITAL LETTER UN +10B4; C; 2D14; # GEORGIAN CAPITAL LETTER PHAR +10B5; C; 2D15; # GEORGIAN CAPITAL LETTER KHAR +10B6; C; 2D16; # GEORGIAN CAPITAL LETTER GHAN +10B7; C; 2D17; # GEORGIAN CAPITAL LETTER QAR +10B8; C; 2D18; # GEORGIAN CAPITAL LETTER SHIN +10B9; C; 2D19; # GEORGIAN CAPITAL LETTER CHIN +10BA; C; 2D1A; # GEORGIAN CAPITAL LETTER CAN +10BB; C; 2D1B; # GEORGIAN CAPITAL LETTER JIL +10BC; C; 2D1C; # GEORGIAN CAPITAL LETTER CIL +10BD; C; 2D1D; # GEORGIAN CAPITAL LETTER CHAR +10BE; C; 2D1E; # GEORGIAN CAPITAL LETTER XAN +10BF; C; 2D1F; # GEORGIAN CAPITAL LETTER JHAN +10C0; C; 2D20; # GEORGIAN CAPITAL LETTER HAE +10C1; C; 2D21; # GEORGIAN CAPITAL LETTER HE +10C2; C; 2D22; # GEORGIAN CAPITAL LETTER HIE +10C3; C; 2D23; # GEORGIAN CAPITAL LETTER WE +10C4; C; 2D24; # GEORGIAN CAPITAL LETTER HAR +10C5; C; 2D25; # GEORGIAN CAPITAL LETTER HOE +10C7; C; 2D27; # GEORGIAN CAPITAL LETTER YN +10CD; C; 2D2D; # GEORGIAN CAPITAL LETTER AEN +13F8; C; 13F0; # CHEROKEE SMALL LETTER YE +13F9; C; 13F1; # CHEROKEE SMALL LETTER YI +13FA; C; 13F2; # CHEROKEE SMALL LETTER YO +13FB; C; 13F3; # CHEROKEE SMALL LETTER YU +13FC; C; 13F4; # CHEROKEE SMALL LETTER YV +13FD; C; 13F5; # CHEROKEE SMALL LETTER MV +1C80; C; 0432; # CYRILLIC SMALL LETTER ROUNDED VE +1C81; C; 0434; # CYRILLIC SMALL LETTER LONG-LEGGED DE +1C82; C; 043E; # CYRILLIC SMALL LETTER NARROW O +1C83; C; 0441; # CYRILLIC SMALL LETTER WIDE ES +1C84; C; 0442; # CYRILLIC SMALL LETTER TALL TE +1C85; C; 0442; # CYRILLIC SMALL LETTER THREE-LEGGED TE +1C86; C; 044A; # CYRILLIC SMALL LETTER TALL HARD SIGN +1C87; C; 0463; # CYRILLIC SMALL LETTER TALL YAT +1C88; C; A64B; # CYRILLIC SMALL LETTER UNBLENDED UK +1C90; C; 10D0; # GEORGIAN MTAVRULI CAPITAL LETTER AN +1C91; C; 10D1; # GEORGIAN MTAVRULI CAPITAL LETTER BAN +1C92; C; 10D2; # GEORGIAN MTAVRULI CAPITAL LETTER GAN +1C93; C; 10D3; # GEORGIAN MTAVRULI CAPITAL LETTER DON +1C94; C; 10D4; # GEORGIAN MTAVRULI CAPITAL LETTER EN +1C95; C; 10D5; # GEORGIAN MTAVRULI CAPITAL LETTER VIN +1C96; C; 10D6; # GEORGIAN MTAVRULI CAPITAL LETTER ZEN +1C97; C; 10D7; # GEORGIAN MTAVRULI CAPITAL LETTER TAN +1C98; C; 10D8; # GEORGIAN MTAVRULI CAPITAL LETTER IN +1C99; C; 10D9; # GEORGIAN MTAVRULI CAPITAL LETTER KAN +1C9A; C; 10DA; # GEORGIAN MTAVRULI CAPITAL LETTER LAS +1C9B; C; 10DB; # GEORGIAN MTAVRULI CAPITAL LETTER MAN +1C9C; C; 10DC; # GEORGIAN MTAVRULI CAPITAL LETTER NAR +1C9D; C; 10DD; # GEORGIAN MTAVRULI CAPITAL LETTER ON +1C9E; C; 10DE; # GEORGIAN MTAVRULI CAPITAL LETTER PAR +1C9F; C; 10DF; # GEORGIAN MTAVRULI CAPITAL LETTER ZHAR +1CA0; C; 10E0; # GEORGIAN MTAVRULI CAPITAL LETTER RAE +1CA1; C; 10E1; # GEORGIAN MTAVRULI CAPITAL LETTER SAN +1CA2; C; 10E2; # GEORGIAN MTAVRULI CAPITAL LETTER TAR +1CA3; C; 10E3; # GEORGIAN MTAVRULI CAPITAL LETTER UN +1CA4; C; 10E4; # GEORGIAN MTAVRULI CAPITAL LETTER PHAR +1CA5; C; 10E5; # GEORGIAN MTAVRULI CAPITAL LETTER KHAR +1CA6; C; 10E6; # GEORGIAN MTAVRULI CAPITAL LETTER GHAN +1CA7; C; 10E7; # GEORGIAN MTAVRULI CAPITAL LETTER QAR +1CA8; C; 10E8; # GEORGIAN MTAVRULI CAPITAL LETTER SHIN +1CA9; C; 10E9; # GEORGIAN MTAVRULI CAPITAL LETTER CHIN +1CAA; C; 10EA; # GEORGIAN MTAVRULI CAPITAL LETTER CAN +1CAB; C; 10EB; # GEORGIAN MTAVRULI CAPITAL LETTER JIL +1CAC; C; 10EC; # GEORGIAN MTAVRULI CAPITAL LETTER CIL +1CAD; C; 10ED; # GEORGIAN MTAVRULI CAPITAL LETTER CHAR +1CAE; C; 10EE; # GEORGIAN MTAVRULI CAPITAL LETTER XAN +1CAF; C; 10EF; # GEORGIAN MTAVRULI CAPITAL LETTER JHAN +1CB0; C; 10F0; # GEORGIAN MTAVRULI CAPITAL LETTER HAE +1CB1; C; 10F1; # GEORGIAN MTAVRULI CAPITAL LETTER HE +1CB2; C; 10F2; # GEORGIAN MTAVRULI CAPITAL LETTER HIE +1CB3; C; 10F3; # GEORGIAN MTAVRULI CAPITAL LETTER WE +1CB4; C; 10F4; # GEORGIAN MTAVRULI CAPITAL LETTER HAR +1CB5; C; 10F5; # GEORGIAN MTAVRULI CAPITAL LETTER HOE +1CB6; C; 10F6; # GEORGIAN MTAVRULI CAPITAL LETTER FI +1CB7; C; 10F7; # GEORGIAN MTAVRULI CAPITAL LETTER YN +1CB8; C; 10F8; # GEORGIAN MTAVRULI CAPITAL LETTER ELIFI +1CB9; C; 10F9; # GEORGIAN MTAVRULI CAPITAL LETTER TURNED GAN +1CBA; C; 10FA; # GEORGIAN MTAVRULI CAPITAL LETTER AIN +1CBD; C; 10FD; # GEORGIAN MTAVRULI CAPITAL LETTER AEN +1CBE; C; 10FE; # GEORGIAN MTAVRULI CAPITAL LETTER HARD SIGN +1CBF; C; 10FF; # GEORGIAN MTAVRULI CAPITAL LETTER LABIAL SIGN +1E00; C; 1E01; # LATIN CAPITAL LETTER A WITH RING BELOW +1E02; C; 1E03; # LATIN CAPITAL LETTER B WITH DOT ABOVE +1E04; C; 1E05; # LATIN CAPITAL LETTER B WITH DOT BELOW +1E06; C; 1E07; # LATIN CAPITAL LETTER B WITH LINE BELOW +1E08; C; 1E09; # LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE +1E0A; C; 1E0B; # LATIN CAPITAL LETTER D WITH DOT ABOVE +1E0C; C; 1E0D; # LATIN CAPITAL LETTER D WITH DOT BELOW +1E0E; C; 1E0F; # LATIN CAPITAL LETTER D WITH LINE BELOW +1E10; C; 1E11; # LATIN CAPITAL LETTER D WITH CEDILLA +1E12; C; 1E13; # LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW +1E14; C; 1E15; # LATIN CAPITAL LETTER E WITH MACRON AND GRAVE +1E16; C; 1E17; # LATIN CAPITAL LETTER E WITH MACRON AND ACUTE +1E18; C; 1E19; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW +1E1A; C; 1E1B; # LATIN CAPITAL LETTER E WITH TILDE BELOW +1E1C; C; 1E1D; # LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE +1E1E; C; 1E1F; # LATIN CAPITAL LETTER F WITH DOT ABOVE +1E20; C; 1E21; # LATIN CAPITAL LETTER G WITH MACRON +1E22; C; 1E23; # LATIN CAPITAL LETTER H WITH DOT ABOVE +1E24; C; 1E25; # LATIN CAPITAL LETTER H WITH DOT BELOW +1E26; C; 1E27; # LATIN CAPITAL LETTER H WITH DIAERESIS +1E28; C; 1E29; # LATIN CAPITAL LETTER H WITH CEDILLA +1E2A; C; 1E2B; # LATIN CAPITAL LETTER H WITH BREVE BELOW +1E2C; C; 1E2D; # LATIN CAPITAL LETTER I WITH TILDE BELOW +1E2E; C; 1E2F; # LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE +1E30; C; 1E31; # LATIN CAPITAL LETTER K WITH ACUTE +1E32; C; 1E33; # LATIN CAPITAL LETTER K WITH DOT BELOW +1E34; C; 1E35; # LATIN CAPITAL LETTER K WITH LINE BELOW +1E36; C; 1E37; # LATIN CAPITAL LETTER L WITH DOT BELOW +1E38; C; 1E39; # LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON +1E3A; C; 1E3B; # LATIN CAPITAL LETTER L WITH LINE BELOW +1E3C; C; 1E3D; # LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW +1E3E; C; 1E3F; # LATIN CAPITAL LETTER M WITH ACUTE +1E40; C; 1E41; # LATIN CAPITAL LETTER M WITH DOT ABOVE +1E42; C; 1E43; # LATIN CAPITAL LETTER M WITH DOT BELOW +1E44; C; 1E45; # LATIN CAPITAL LETTER N WITH DOT ABOVE +1E46; C; 1E47; # LATIN CAPITAL LETTER N WITH DOT BELOW +1E48; C; 1E49; # LATIN CAPITAL LETTER N WITH LINE BELOW +1E4A; C; 1E4B; # LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW +1E4C; C; 1E4D; # LATIN CAPITAL LETTER O WITH TILDE AND ACUTE +1E4E; C; 1E4F; # LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS +1E50; C; 1E51; # LATIN CAPITAL LETTER O WITH MACRON AND GRAVE +1E52; C; 1E53; # LATIN CAPITAL LETTER O WITH MACRON AND ACUTE +1E54; C; 1E55; # LATIN CAPITAL LETTER P WITH ACUTE +1E56; C; 1E57; # LATIN CAPITAL LETTER P WITH DOT ABOVE +1E58; C; 1E59; # LATIN CAPITAL LETTER R WITH DOT ABOVE +1E5A; C; 1E5B; # LATIN CAPITAL LETTER R WITH DOT BELOW +1E5C; C; 1E5D; # LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON +1E5E; C; 1E5F; # LATIN CAPITAL LETTER R WITH LINE BELOW +1E60; C; 1E61; # LATIN CAPITAL LETTER S WITH DOT ABOVE +1E62; C; 1E63; # LATIN CAPITAL LETTER S WITH DOT BELOW +1E64; C; 1E65; # LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE +1E66; C; 1E67; # LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE +1E68; C; 1E69; # LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE +1E6A; C; 1E6B; # LATIN CAPITAL LETTER T WITH DOT ABOVE +1E6C; C; 1E6D; # LATIN CAPITAL LETTER T WITH DOT BELOW +1E6E; C; 1E6F; # LATIN CAPITAL LETTER T WITH LINE BELOW +1E70; C; 1E71; # LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW +1E72; C; 1E73; # LATIN CAPITAL LETTER U WITH DIAERESIS BELOW +1E74; C; 1E75; # LATIN CAPITAL LETTER U WITH TILDE BELOW +1E76; C; 1E77; # LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW +1E78; C; 1E79; # LATIN CAPITAL LETTER U WITH TILDE AND ACUTE +1E7A; C; 1E7B; # LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS +1E7C; C; 1E7D; # LATIN CAPITAL LETTER V WITH TILDE +1E7E; C; 1E7F; # LATIN CAPITAL LETTER V WITH DOT BELOW +1E80; C; 1E81; # LATIN CAPITAL LETTER W WITH GRAVE +1E82; C; 1E83; # LATIN CAPITAL LETTER W WITH ACUTE +1E84; C; 1E85; # LATIN CAPITAL LETTER W WITH DIAERESIS +1E86; C; 1E87; # LATIN CAPITAL LETTER W WITH DOT ABOVE +1E88; C; 1E89; # LATIN CAPITAL LETTER W WITH DOT BELOW +1E8A; C; 1E8B; # LATIN CAPITAL LETTER X WITH DOT ABOVE +1E8C; C; 1E8D; # LATIN CAPITAL LETTER X WITH DIAERESIS +1E8E; C; 1E8F; # LATIN CAPITAL LETTER Y WITH DOT ABOVE +1E90; C; 1E91; # LATIN CAPITAL LETTER Z WITH CIRCUMFLEX +1E92; C; 1E93; # LATIN CAPITAL LETTER Z WITH DOT BELOW +1E94; C; 1E95; # LATIN CAPITAL LETTER Z WITH LINE BELOW +1E96; F; 0068 0331; # LATIN SMALL LETTER H WITH LINE BELOW +1E97; F; 0074 0308; # LATIN SMALL LETTER T WITH DIAERESIS +1E98; F; 0077 030A; # LATIN SMALL LETTER W WITH RING ABOVE +1E99; F; 0079 030A; # LATIN SMALL LETTER Y WITH RING ABOVE +1E9A; F; 0061 02BE; # LATIN SMALL LETTER A WITH RIGHT HALF RING +1E9B; C; 1E61; # LATIN SMALL LETTER LONG S WITH DOT ABOVE +1E9E; F; 0073 0073; # LATIN CAPITAL LETTER SHARP S +1E9E; S; 00DF; # LATIN CAPITAL LETTER SHARP S +1EA0; C; 1EA1; # LATIN CAPITAL LETTER A WITH DOT BELOW +1EA2; C; 1EA3; # LATIN CAPITAL LETTER A WITH HOOK ABOVE +1EA4; C; 1EA5; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE +1EA6; C; 1EA7; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE +1EA8; C; 1EA9; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE +1EAA; C; 1EAB; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE +1EAC; C; 1EAD; # LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW +1EAE; C; 1EAF; # LATIN CAPITAL LETTER A WITH BREVE AND ACUTE +1EB0; C; 1EB1; # LATIN CAPITAL LETTER A WITH BREVE AND GRAVE +1EB2; C; 1EB3; # LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE +1EB4; C; 1EB5; # LATIN CAPITAL LETTER A WITH BREVE AND TILDE +1EB6; C; 1EB7; # LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW +1EB8; C; 1EB9; # LATIN CAPITAL LETTER E WITH DOT BELOW +1EBA; C; 1EBB; # LATIN CAPITAL LETTER E WITH HOOK ABOVE +1EBC; C; 1EBD; # LATIN CAPITAL LETTER E WITH TILDE +1EBE; C; 1EBF; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE +1EC0; C; 1EC1; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE +1EC2; C; 1EC3; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE +1EC4; C; 1EC5; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE +1EC6; C; 1EC7; # LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW +1EC8; C; 1EC9; # LATIN CAPITAL LETTER I WITH HOOK ABOVE +1ECA; C; 1ECB; # LATIN CAPITAL LETTER I WITH DOT BELOW +1ECC; C; 1ECD; # LATIN CAPITAL LETTER O WITH DOT BELOW +1ECE; C; 1ECF; # LATIN CAPITAL LETTER O WITH HOOK ABOVE +1ED0; C; 1ED1; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE +1ED2; C; 1ED3; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE +1ED4; C; 1ED5; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE +1ED6; C; 1ED7; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE +1ED8; C; 1ED9; # LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW +1EDA; C; 1EDB; # LATIN CAPITAL LETTER O WITH HORN AND ACUTE +1EDC; C; 1EDD; # LATIN CAPITAL LETTER O WITH HORN AND GRAVE +1EDE; C; 1EDF; # LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE +1EE0; C; 1EE1; # LATIN CAPITAL LETTER O WITH HORN AND TILDE +1EE2; C; 1EE3; # LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW +1EE4; C; 1EE5; # LATIN CAPITAL LETTER U WITH DOT BELOW +1EE6; C; 1EE7; # LATIN CAPITAL LETTER U WITH HOOK ABOVE +1EE8; C; 1EE9; # LATIN CAPITAL LETTER U WITH HORN AND ACUTE +1EEA; C; 1EEB; # LATIN CAPITAL LETTER U WITH HORN AND GRAVE +1EEC; C; 1EED; # LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE +1EEE; C; 1EEF; # LATIN CAPITAL LETTER U WITH HORN AND TILDE +1EF0; C; 1EF1; # LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW +1EF2; C; 1EF3; # LATIN CAPITAL LETTER Y WITH GRAVE +1EF4; C; 1EF5; # LATIN CAPITAL LETTER Y WITH DOT BELOW +1EF6; C; 1EF7; # LATIN CAPITAL LETTER Y WITH HOOK ABOVE +1EF8; C; 1EF9; # LATIN CAPITAL LETTER Y WITH TILDE +1EFA; C; 1EFB; # LATIN CAPITAL LETTER MIDDLE-WELSH LL +1EFC; C; 1EFD; # LATIN CAPITAL LETTER MIDDLE-WELSH V +1EFE; C; 1EFF; # LATIN CAPITAL LETTER Y WITH LOOP +1F08; C; 1F00; # GREEK CAPITAL LETTER ALPHA WITH PSILI +1F09; C; 1F01; # GREEK CAPITAL LETTER ALPHA WITH DASIA +1F0A; C; 1F02; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA +1F0B; C; 1F03; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA +1F0C; C; 1F04; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA +1F0D; C; 1F05; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA +1F0E; C; 1F06; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI +1F0F; C; 1F07; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI +1F18; C; 1F10; # GREEK CAPITAL LETTER EPSILON WITH PSILI +1F19; C; 1F11; # GREEK CAPITAL LETTER EPSILON WITH DASIA +1F1A; C; 1F12; # GREEK CAPITAL LETTER EPSILON WITH PSILI AND VARIA +1F1B; C; 1F13; # GREEK CAPITAL LETTER EPSILON WITH DASIA AND VARIA +1F1C; C; 1F14; # GREEK CAPITAL LETTER EPSILON WITH PSILI AND OXIA +1F1D; C; 1F15; # GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA +1F28; C; 1F20; # GREEK CAPITAL LETTER ETA WITH PSILI +1F29; C; 1F21; # GREEK CAPITAL LETTER ETA WITH DASIA +1F2A; C; 1F22; # GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA +1F2B; C; 1F23; # GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA +1F2C; C; 1F24; # GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA +1F2D; C; 1F25; # GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA +1F2E; C; 1F26; # GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI +1F2F; C; 1F27; # GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI +1F38; C; 1F30; # GREEK CAPITAL LETTER IOTA WITH PSILI +1F39; C; 1F31; # GREEK CAPITAL LETTER IOTA WITH DASIA +1F3A; C; 1F32; # GREEK CAPITAL LETTER IOTA WITH PSILI AND VARIA +1F3B; C; 1F33; # GREEK CAPITAL LETTER IOTA WITH DASIA AND VARIA +1F3C; C; 1F34; # GREEK CAPITAL LETTER IOTA WITH PSILI AND OXIA +1F3D; C; 1F35; # GREEK CAPITAL LETTER IOTA WITH DASIA AND OXIA +1F3E; C; 1F36; # GREEK CAPITAL LETTER IOTA WITH PSILI AND PERISPOMENI +1F3F; C; 1F37; # GREEK CAPITAL LETTER IOTA WITH DASIA AND PERISPOMENI +1F48; C; 1F40; # GREEK CAPITAL LETTER OMICRON WITH PSILI +1F49; C; 1F41; # GREEK CAPITAL LETTER OMICRON WITH DASIA +1F4A; C; 1F42; # GREEK CAPITAL LETTER OMICRON WITH PSILI AND VARIA +1F4B; C; 1F43; # GREEK CAPITAL LETTER OMICRON WITH DASIA AND VARIA +1F4C; C; 1F44; # GREEK CAPITAL LETTER OMICRON WITH PSILI AND OXIA +1F4D; C; 1F45; # GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA +1F50; F; 03C5 0313; # GREEK SMALL LETTER UPSILON WITH PSILI +1F52; F; 03C5 0313 0300; # GREEK SMALL LETTER UPSILON WITH PSILI AND VARIA +1F54; F; 03C5 0313 0301; # GREEK SMALL LETTER UPSILON WITH PSILI AND OXIA +1F56; F; 03C5 0313 0342; # GREEK SMALL LETTER UPSILON WITH PSILI AND PERISPOMENI +1F59; C; 1F51; # GREEK CAPITAL LETTER UPSILON WITH DASIA +1F5B; C; 1F53; # GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA +1F5D; C; 1F55; # GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA +1F5F; C; 1F57; # GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI +1F68; C; 1F60; # GREEK CAPITAL LETTER OMEGA WITH PSILI +1F69; C; 1F61; # GREEK CAPITAL LETTER OMEGA WITH DASIA +1F6A; C; 1F62; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA +1F6B; C; 1F63; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA +1F6C; C; 1F64; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA +1F6D; C; 1F65; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA +1F6E; C; 1F66; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI +1F6F; C; 1F67; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI +1F80; F; 1F00 03B9; # GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI +1F81; F; 1F01 03B9; # GREEK SMALL LETTER ALPHA WITH DASIA AND YPOGEGRAMMENI +1F82; F; 1F02 03B9; # GREEK SMALL LETTER ALPHA WITH PSILI AND VARIA AND YPOGEGRAMMENI +1F83; F; 1F03 03B9; # GREEK SMALL LETTER ALPHA WITH DASIA AND VARIA AND YPOGEGRAMMENI +1F84; F; 1F04 03B9; # GREEK SMALL LETTER ALPHA WITH PSILI AND OXIA AND YPOGEGRAMMENI +1F85; F; 1F05 03B9; # GREEK SMALL LETTER ALPHA WITH DASIA AND OXIA AND YPOGEGRAMMENI +1F86; F; 1F06 03B9; # GREEK SMALL LETTER ALPHA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI +1F87; F; 1F07 03B9; # GREEK SMALL LETTER ALPHA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI +1F88; F; 1F00 03B9; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI +1F88; S; 1F80; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI +1F89; F; 1F01 03B9; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI +1F89; S; 1F81; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI +1F8A; F; 1F02 03B9; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI +1F8A; S; 1F82; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI +1F8B; F; 1F03 03B9; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI +1F8B; S; 1F83; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI +1F8C; F; 1F04 03B9; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI +1F8C; S; 1F84; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI +1F8D; F; 1F05 03B9; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI +1F8D; S; 1F85; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI +1F8E; F; 1F06 03B9; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI +1F8E; S; 1F86; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI +1F8F; F; 1F07 03B9; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI +1F8F; S; 1F87; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI +1F90; F; 1F20 03B9; # GREEK SMALL LETTER ETA WITH PSILI AND YPOGEGRAMMENI +1F91; F; 1F21 03B9; # GREEK SMALL LETTER ETA WITH DASIA AND YPOGEGRAMMENI +1F92; F; 1F22 03B9; # GREEK SMALL LETTER ETA WITH PSILI AND VARIA AND YPOGEGRAMMENI +1F93; F; 1F23 03B9; # GREEK SMALL LETTER ETA WITH DASIA AND VARIA AND YPOGEGRAMMENI +1F94; F; 1F24 03B9; # GREEK SMALL LETTER ETA WITH PSILI AND OXIA AND YPOGEGRAMMENI +1F95; F; 1F25 03B9; # GREEK SMALL LETTER ETA WITH DASIA AND OXIA AND YPOGEGRAMMENI +1F96; F; 1F26 03B9; # GREEK SMALL LETTER ETA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI +1F97; F; 1F27 03B9; # GREEK SMALL LETTER ETA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI +1F98; F; 1F20 03B9; # GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI +1F98; S; 1F90; # GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI +1F99; F; 1F21 03B9; # GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI +1F99; S; 1F91; # GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI +1F9A; F; 1F22 03B9; # GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI +1F9A; S; 1F92; # GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI +1F9B; F; 1F23 03B9; # GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI +1F9B; S; 1F93; # GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI +1F9C; F; 1F24 03B9; # GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI +1F9C; S; 1F94; # GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI +1F9D; F; 1F25 03B9; # GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI +1F9D; S; 1F95; # GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI +1F9E; F; 1F26 03B9; # GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI +1F9E; S; 1F96; # GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI +1F9F; F; 1F27 03B9; # GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI +1F9F; S; 1F97; # GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI +1FA0; F; 1F60 03B9; # GREEK SMALL LETTER OMEGA WITH PSILI AND YPOGEGRAMMENI +1FA1; F; 1F61 03B9; # GREEK SMALL LETTER OMEGA WITH DASIA AND YPOGEGRAMMENI +1FA2; F; 1F62 03B9; # GREEK SMALL LETTER OMEGA WITH PSILI AND VARIA AND YPOGEGRAMMENI +1FA3; F; 1F63 03B9; # GREEK SMALL LETTER OMEGA WITH DASIA AND VARIA AND YPOGEGRAMMENI +1FA4; F; 1F64 03B9; # GREEK SMALL LETTER OMEGA WITH PSILI AND OXIA AND YPOGEGRAMMENI +1FA5; F; 1F65 03B9; # GREEK SMALL LETTER OMEGA WITH DASIA AND OXIA AND YPOGEGRAMMENI +1FA6; F; 1F66 03B9; # GREEK SMALL LETTER OMEGA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI +1FA7; F; 1F67 03B9; # GREEK SMALL LETTER OMEGA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI +1FA8; F; 1F60 03B9; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI +1FA8; S; 1FA0; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI +1FA9; F; 1F61 03B9; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI +1FA9; S; 1FA1; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI +1FAA; F; 1F62 03B9; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI +1FAA; S; 1FA2; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI +1FAB; F; 1F63 03B9; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI +1FAB; S; 1FA3; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI +1FAC; F; 1F64 03B9; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI +1FAC; S; 1FA4; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI +1FAD; F; 1F65 03B9; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI +1FAD; S; 1FA5; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI +1FAE; F; 1F66 03B9; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI +1FAE; S; 1FA6; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI +1FAF; F; 1F67 03B9; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI +1FAF; S; 1FA7; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI +1FB2; F; 1F70 03B9; # GREEK SMALL LETTER ALPHA WITH VARIA AND YPOGEGRAMMENI +1FB3; F; 03B1 03B9; # GREEK SMALL LETTER ALPHA WITH YPOGEGRAMMENI +1FB4; F; 03AC 03B9; # GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI +1FB6; F; 03B1 0342; # GREEK SMALL LETTER ALPHA WITH PERISPOMENI +1FB7; F; 03B1 0342 03B9; # GREEK SMALL LETTER ALPHA WITH PERISPOMENI AND YPOGEGRAMMENI +1FB8; C; 1FB0; # GREEK CAPITAL LETTER ALPHA WITH VRACHY +1FB9; C; 1FB1; # GREEK CAPITAL LETTER ALPHA WITH MACRON +1FBA; C; 1F70; # GREEK CAPITAL LETTER ALPHA WITH VARIA +1FBB; C; 1F71; # GREEK CAPITAL LETTER ALPHA WITH OXIA +1FBC; F; 03B1 03B9; # GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI +1FBC; S; 1FB3; # GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI +1FBE; C; 03B9; # GREEK PROSGEGRAMMENI +1FC2; F; 1F74 03B9; # GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI +1FC3; F; 03B7 03B9; # GREEK SMALL LETTER ETA WITH YPOGEGRAMMENI +1FC4; F; 03AE 03B9; # GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI +1FC6; F; 03B7 0342; # GREEK SMALL LETTER ETA WITH PERISPOMENI +1FC7; F; 03B7 0342 03B9; # GREEK SMALL LETTER ETA WITH PERISPOMENI AND YPOGEGRAMMENI +1FC8; C; 1F72; # GREEK CAPITAL LETTER EPSILON WITH VARIA +1FC9; C; 1F73; # GREEK CAPITAL LETTER EPSILON WITH OXIA +1FCA; C; 1F74; # GREEK CAPITAL LETTER ETA WITH VARIA +1FCB; C; 1F75; # GREEK CAPITAL LETTER ETA WITH OXIA +1FCC; F; 03B7 03B9; # GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI +1FCC; S; 1FC3; # GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI +1FD2; F; 03B9 0308 0300; # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND VARIA +1FD3; F; 03B9 0308 0301; # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA +1FD6; F; 03B9 0342; # GREEK SMALL LETTER IOTA WITH PERISPOMENI +1FD7; F; 03B9 0308 0342; # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND PERISPOMENI +1FD8; C; 1FD0; # GREEK CAPITAL LETTER IOTA WITH VRACHY +1FD9; C; 1FD1; # GREEK CAPITAL LETTER IOTA WITH MACRON +1FDA; C; 1F76; # GREEK CAPITAL LETTER IOTA WITH VARIA +1FDB; C; 1F77; # GREEK CAPITAL LETTER IOTA WITH OXIA +1FE2; F; 03C5 0308 0300; # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND VARIA +1FE3; F; 03C5 0308 0301; # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND OXIA +1FE4; F; 03C1 0313; # GREEK SMALL LETTER RHO WITH PSILI +1FE6; F; 03C5 0342; # GREEK SMALL LETTER UPSILON WITH PERISPOMENI +1FE7; F; 03C5 0308 0342; # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND PERISPOMENI +1FE8; C; 1FE0; # GREEK CAPITAL LETTER UPSILON WITH VRACHY +1FE9; C; 1FE1; # GREEK CAPITAL LETTER UPSILON WITH MACRON +1FEA; C; 1F7A; # GREEK CAPITAL LETTER UPSILON WITH VARIA +1FEB; C; 1F7B; # GREEK CAPITAL LETTER UPSILON WITH OXIA +1FEC; C; 1FE5; # GREEK CAPITAL LETTER RHO WITH DASIA +1FF2; F; 1F7C 03B9; # GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI +1FF3; F; 03C9 03B9; # GREEK SMALL LETTER OMEGA WITH YPOGEGRAMMENI +1FF4; F; 03CE 03B9; # GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI +1FF6; F; 03C9 0342; # GREEK SMALL LETTER OMEGA WITH PERISPOMENI +1FF7; F; 03C9 0342 03B9; # GREEK SMALL LETTER OMEGA WITH PERISPOMENI AND YPOGEGRAMMENI +1FF8; C; 1F78; # GREEK CAPITAL LETTER OMICRON WITH VARIA +1FF9; C; 1F79; # GREEK CAPITAL LETTER OMICRON WITH OXIA +1FFA; C; 1F7C; # GREEK CAPITAL LETTER OMEGA WITH VARIA +1FFB; C; 1F7D; # GREEK CAPITAL LETTER OMEGA WITH OXIA +1FFC; F; 03C9 03B9; # GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI +1FFC; S; 1FF3; # GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI +2126; C; 03C9; # OHM SIGN +212A; C; 006B; # KELVIN SIGN +212B; C; 00E5; # ANGSTROM SIGN +2132; C; 214E; # TURNED CAPITAL F +2160; C; 2170; # ROMAN NUMERAL ONE +2161; C; 2171; # ROMAN NUMERAL TWO +2162; C; 2172; # ROMAN NUMERAL THREE +2163; C; 2173; # ROMAN NUMERAL FOUR +2164; C; 2174; # ROMAN NUMERAL FIVE +2165; C; 2175; # ROMAN NUMERAL SIX +2166; C; 2176; # ROMAN NUMERAL SEVEN +2167; C; 2177; # ROMAN NUMERAL EIGHT +2168; C; 2178; # ROMAN NUMERAL NINE +2169; C; 2179; # ROMAN NUMERAL TEN +216A; C; 217A; # ROMAN NUMERAL ELEVEN +216B; C; 217B; # ROMAN NUMERAL TWELVE +216C; C; 217C; # ROMAN NUMERAL FIFTY +216D; C; 217D; # ROMAN NUMERAL ONE HUNDRED +216E; C; 217E; # ROMAN NUMERAL FIVE HUNDRED +216F; C; 217F; # ROMAN NUMERAL ONE THOUSAND +2183; C; 2184; # ROMAN NUMERAL REVERSED ONE HUNDRED +24B6; C; 24D0; # CIRCLED LATIN CAPITAL LETTER A +24B7; C; 24D1; # CIRCLED LATIN CAPITAL LETTER B +24B8; C; 24D2; # CIRCLED LATIN CAPITAL LETTER C +24B9; C; 24D3; # CIRCLED LATIN CAPITAL LETTER D +24BA; C; 24D4; # CIRCLED LATIN CAPITAL LETTER E +24BB; C; 24D5; # CIRCLED LATIN CAPITAL LETTER F +24BC; C; 24D6; # CIRCLED LATIN CAPITAL LETTER G +24BD; C; 24D7; # CIRCLED LATIN CAPITAL LETTER H +24BE; C; 24D8; # CIRCLED LATIN CAPITAL LETTER I +24BF; C; 24D9; # CIRCLED LATIN CAPITAL LETTER J +24C0; C; 24DA; # CIRCLED LATIN CAPITAL LETTER K +24C1; C; 24DB; # CIRCLED LATIN CAPITAL LETTER L +24C2; C; 24DC; # CIRCLED LATIN CAPITAL LETTER M +24C3; C; 24DD; # CIRCLED LATIN CAPITAL LETTER N +24C4; C; 24DE; # CIRCLED LATIN CAPITAL LETTER O +24C5; C; 24DF; # CIRCLED LATIN CAPITAL LETTER P +24C6; C; 24E0; # CIRCLED LATIN CAPITAL LETTER Q +24C7; C; 24E1; # CIRCLED LATIN CAPITAL LETTER R +24C8; C; 24E2; # CIRCLED LATIN CAPITAL LETTER S +24C9; C; 24E3; # CIRCLED LATIN CAPITAL LETTER T +24CA; C; 24E4; # CIRCLED LATIN CAPITAL LETTER U +24CB; C; 24E5; # CIRCLED LATIN CAPITAL LETTER V +24CC; C; 24E6; # CIRCLED LATIN CAPITAL LETTER W +24CD; C; 24E7; # CIRCLED LATIN CAPITAL LETTER X +24CE; C; 24E8; # CIRCLED LATIN CAPITAL LETTER Y +24CF; C; 24E9; # CIRCLED LATIN CAPITAL LETTER Z +2C00; C; 2C30; # GLAGOLITIC CAPITAL LETTER AZU +2C01; C; 2C31; # GLAGOLITIC CAPITAL LETTER BUKY +2C02; C; 2C32; # GLAGOLITIC CAPITAL LETTER VEDE +2C03; C; 2C33; # GLAGOLITIC CAPITAL LETTER GLAGOLI +2C04; C; 2C34; # GLAGOLITIC CAPITAL LETTER DOBRO +2C05; C; 2C35; # GLAGOLITIC CAPITAL LETTER YESTU +2C06; C; 2C36; # GLAGOLITIC CAPITAL LETTER ZHIVETE +2C07; C; 2C37; # GLAGOLITIC CAPITAL LETTER DZELO +2C08; C; 2C38; # GLAGOLITIC CAPITAL LETTER ZEMLJA +2C09; C; 2C39; # GLAGOLITIC CAPITAL LETTER IZHE +2C0A; C; 2C3A; # GLAGOLITIC CAPITAL LETTER INITIAL IZHE +2C0B; C; 2C3B; # GLAGOLITIC CAPITAL LETTER I +2C0C; C; 2C3C; # GLAGOLITIC CAPITAL LETTER DJERVI +2C0D; C; 2C3D; # GLAGOLITIC CAPITAL LETTER KAKO +2C0E; C; 2C3E; # GLAGOLITIC CAPITAL LETTER LJUDIJE +2C0F; C; 2C3F; # GLAGOLITIC CAPITAL LETTER MYSLITE +2C10; C; 2C40; # GLAGOLITIC CAPITAL LETTER NASHI +2C11; C; 2C41; # GLAGOLITIC CAPITAL LETTER ONU +2C12; C; 2C42; # GLAGOLITIC CAPITAL LETTER POKOJI +2C13; C; 2C43; # GLAGOLITIC CAPITAL LETTER RITSI +2C14; C; 2C44; # GLAGOLITIC CAPITAL LETTER SLOVO +2C15; C; 2C45; # GLAGOLITIC CAPITAL LETTER TVRIDO +2C16; C; 2C46; # GLAGOLITIC CAPITAL LETTER UKU +2C17; C; 2C47; # GLAGOLITIC CAPITAL LETTER FRITU +2C18; C; 2C48; # GLAGOLITIC CAPITAL LETTER HERU +2C19; C; 2C49; # GLAGOLITIC CAPITAL LETTER OTU +2C1A; C; 2C4A; # GLAGOLITIC CAPITAL LETTER PE +2C1B; C; 2C4B; # GLAGOLITIC CAPITAL LETTER SHTA +2C1C; C; 2C4C; # GLAGOLITIC CAPITAL LETTER TSI +2C1D; C; 2C4D; # GLAGOLITIC CAPITAL LETTER CHRIVI +2C1E; C; 2C4E; # GLAGOLITIC CAPITAL LETTER SHA +2C1F; C; 2C4F; # GLAGOLITIC CAPITAL LETTER YERU +2C20; C; 2C50; # GLAGOLITIC CAPITAL LETTER YERI +2C21; C; 2C51; # GLAGOLITIC CAPITAL LETTER YATI +2C22; C; 2C52; # GLAGOLITIC CAPITAL LETTER SPIDERY HA +2C23; C; 2C53; # GLAGOLITIC CAPITAL LETTER YU +2C24; C; 2C54; # GLAGOLITIC CAPITAL LETTER SMALL YUS +2C25; C; 2C55; # GLAGOLITIC CAPITAL LETTER SMALL YUS WITH TAIL +2C26; C; 2C56; # GLAGOLITIC CAPITAL LETTER YO +2C27; C; 2C57; # GLAGOLITIC CAPITAL LETTER IOTATED SMALL YUS +2C28; C; 2C58; # GLAGOLITIC CAPITAL LETTER BIG YUS +2C29; C; 2C59; # GLAGOLITIC CAPITAL LETTER IOTATED BIG YUS +2C2A; C; 2C5A; # GLAGOLITIC CAPITAL LETTER FITA +2C2B; C; 2C5B; # GLAGOLITIC CAPITAL LETTER IZHITSA +2C2C; C; 2C5C; # GLAGOLITIC CAPITAL LETTER SHTAPIC +2C2D; C; 2C5D; # GLAGOLITIC CAPITAL LETTER TROKUTASTI A +2C2E; C; 2C5E; # GLAGOLITIC CAPITAL LETTER LATINATE MYSLITE +2C2F; C; 2C5F; # GLAGOLITIC CAPITAL LETTER CAUDATE CHRIVI +2C60; C; 2C61; # LATIN CAPITAL LETTER L WITH DOUBLE BAR +2C62; C; 026B; # LATIN CAPITAL LETTER L WITH MIDDLE TILDE +2C63; C; 1D7D; # LATIN CAPITAL LETTER P WITH STROKE +2C64; C; 027D; # LATIN CAPITAL LETTER R WITH TAIL +2C67; C; 2C68; # LATIN CAPITAL LETTER H WITH DESCENDER +2C69; C; 2C6A; # LATIN CAPITAL LETTER K WITH DESCENDER +2C6B; C; 2C6C; # LATIN CAPITAL LETTER Z WITH DESCENDER +2C6D; C; 0251; # LATIN CAPITAL LETTER ALPHA +2C6E; C; 0271; # LATIN CAPITAL LETTER M WITH HOOK +2C6F; C; 0250; # LATIN CAPITAL LETTER TURNED A +2C70; C; 0252; # LATIN CAPITAL LETTER TURNED ALPHA +2C72; C; 2C73; # LATIN CAPITAL LETTER W WITH HOOK +2C75; C; 2C76; # LATIN CAPITAL LETTER HALF H +2C7E; C; 023F; # LATIN CAPITAL LETTER S WITH SWASH TAIL +2C7F; C; 0240; # LATIN CAPITAL LETTER Z WITH SWASH TAIL +2C80; C; 2C81; # COPTIC CAPITAL LETTER ALFA +2C82; C; 2C83; # COPTIC CAPITAL LETTER VIDA +2C84; C; 2C85; # COPTIC CAPITAL LETTER GAMMA +2C86; C; 2C87; # COPTIC CAPITAL LETTER DALDA +2C88; C; 2C89; # COPTIC CAPITAL LETTER EIE +2C8A; C; 2C8B; # COPTIC CAPITAL LETTER SOU +2C8C; C; 2C8D; # COPTIC CAPITAL LETTER ZATA +2C8E; C; 2C8F; # COPTIC CAPITAL LETTER HATE +2C90; C; 2C91; # COPTIC CAPITAL LETTER THETHE +2C92; C; 2C93; # COPTIC CAPITAL LETTER IAUDA +2C94; C; 2C95; # COPTIC CAPITAL LETTER KAPA +2C96; C; 2C97; # COPTIC CAPITAL LETTER LAULA +2C98; C; 2C99; # COPTIC CAPITAL LETTER MI +2C9A; C; 2C9B; # COPTIC CAPITAL LETTER NI +2C9C; C; 2C9D; # COPTIC CAPITAL LETTER KSI +2C9E; C; 2C9F; # COPTIC CAPITAL LETTER O +2CA0; C; 2CA1; # COPTIC CAPITAL LETTER PI +2CA2; C; 2CA3; # COPTIC CAPITAL LETTER RO +2CA4; C; 2CA5; # COPTIC CAPITAL LETTER SIMA +2CA6; C; 2CA7; # COPTIC CAPITAL LETTER TAU +2CA8; C; 2CA9; # COPTIC CAPITAL LETTER UA +2CAA; C; 2CAB; # COPTIC CAPITAL LETTER FI +2CAC; C; 2CAD; # COPTIC CAPITAL LETTER KHI +2CAE; C; 2CAF; # COPTIC CAPITAL LETTER PSI +2CB0; C; 2CB1; # COPTIC CAPITAL LETTER OOU +2CB2; C; 2CB3; # COPTIC CAPITAL LETTER DIALECT-P ALEF +2CB4; C; 2CB5; # COPTIC CAPITAL LETTER OLD COPTIC AIN +2CB6; C; 2CB7; # COPTIC CAPITAL LETTER CRYPTOGRAMMIC EIE +2CB8; C; 2CB9; # COPTIC CAPITAL LETTER DIALECT-P KAPA +2CBA; C; 2CBB; # COPTIC CAPITAL LETTER DIALECT-P NI +2CBC; C; 2CBD; # COPTIC CAPITAL LETTER CRYPTOGRAMMIC NI +2CBE; C; 2CBF; # COPTIC CAPITAL LETTER OLD COPTIC OOU +2CC0; C; 2CC1; # COPTIC CAPITAL LETTER SAMPI +2CC2; C; 2CC3; # COPTIC CAPITAL LETTER CROSSED SHEI +2CC4; C; 2CC5; # COPTIC CAPITAL LETTER OLD COPTIC SHEI +2CC6; C; 2CC7; # COPTIC CAPITAL LETTER OLD COPTIC ESH +2CC8; C; 2CC9; # COPTIC CAPITAL LETTER AKHMIMIC KHEI +2CCA; C; 2CCB; # COPTIC CAPITAL LETTER DIALECT-P HORI +2CCC; C; 2CCD; # COPTIC CAPITAL LETTER OLD COPTIC HORI +2CCE; C; 2CCF; # COPTIC CAPITAL LETTER OLD COPTIC HA +2CD0; C; 2CD1; # COPTIC CAPITAL LETTER L-SHAPED HA +2CD2; C; 2CD3; # COPTIC CAPITAL LETTER OLD COPTIC HEI +2CD4; C; 2CD5; # COPTIC CAPITAL LETTER OLD COPTIC HAT +2CD6; C; 2CD7; # COPTIC CAPITAL LETTER OLD COPTIC GANGIA +2CD8; C; 2CD9; # COPTIC CAPITAL LETTER OLD COPTIC DJA +2CDA; C; 2CDB; # COPTIC CAPITAL LETTER OLD COPTIC SHIMA +2CDC; C; 2CDD; # COPTIC CAPITAL LETTER OLD NUBIAN SHIMA +2CDE; C; 2CDF; # COPTIC CAPITAL LETTER OLD NUBIAN NGI +2CE0; C; 2CE1; # COPTIC CAPITAL LETTER OLD NUBIAN NYI +2CE2; C; 2CE3; # COPTIC CAPITAL LETTER OLD NUBIAN WAU +2CEB; C; 2CEC; # COPTIC CAPITAL LETTER CRYPTOGRAMMIC SHEI +2CED; C; 2CEE; # COPTIC CAPITAL LETTER CRYPTOGRAMMIC GANGIA +2CF2; C; 2CF3; # COPTIC CAPITAL LETTER BOHAIRIC KHEI +A640; C; A641; # CYRILLIC CAPITAL LETTER ZEMLYA +A642; C; A643; # CYRILLIC CAPITAL LETTER DZELO +A644; C; A645; # CYRILLIC CAPITAL LETTER REVERSED DZE +A646; C; A647; # CYRILLIC CAPITAL LETTER IOTA +A648; C; A649; # CYRILLIC CAPITAL LETTER DJERV +A64A; C; A64B; # CYRILLIC CAPITAL LETTER MONOGRAPH UK +A64C; C; A64D; # CYRILLIC CAPITAL LETTER BROAD OMEGA +A64E; C; A64F; # CYRILLIC CAPITAL LETTER NEUTRAL YER +A650; C; A651; # CYRILLIC CAPITAL LETTER YERU WITH BACK YER +A652; C; A653; # CYRILLIC CAPITAL LETTER IOTIFIED YAT +A654; C; A655; # CYRILLIC CAPITAL LETTER REVERSED YU +A656; C; A657; # CYRILLIC CAPITAL LETTER IOTIFIED A +A658; C; A659; # CYRILLIC CAPITAL LETTER CLOSED LITTLE YUS +A65A; C; A65B; # CYRILLIC CAPITAL LETTER BLENDED YUS +A65C; C; A65D; # CYRILLIC CAPITAL LETTER IOTIFIED CLOSED LITTLE YUS +A65E; C; A65F; # CYRILLIC CAPITAL LETTER YN +A660; C; A661; # CYRILLIC CAPITAL LETTER REVERSED TSE +A662; C; A663; # CYRILLIC CAPITAL LETTER SOFT DE +A664; C; A665; # CYRILLIC CAPITAL LETTER SOFT EL +A666; C; A667; # CYRILLIC CAPITAL LETTER SOFT EM +A668; C; A669; # CYRILLIC CAPITAL LETTER MONOCULAR O +A66A; C; A66B; # CYRILLIC CAPITAL LETTER BINOCULAR O +A66C; C; A66D; # CYRILLIC CAPITAL LETTER DOUBLE MONOCULAR O +A680; C; A681; # CYRILLIC CAPITAL LETTER DWE +A682; C; A683; # CYRILLIC CAPITAL LETTER DZWE +A684; C; A685; # CYRILLIC CAPITAL LETTER ZHWE +A686; C; A687; # CYRILLIC CAPITAL LETTER CCHE +A688; C; A689; # CYRILLIC CAPITAL LETTER DZZE +A68A; C; A68B; # CYRILLIC CAPITAL LETTER TE WITH MIDDLE HOOK +A68C; C; A68D; # CYRILLIC CAPITAL LETTER TWE +A68E; C; A68F; # CYRILLIC CAPITAL LETTER TSWE +A690; C; A691; # CYRILLIC CAPITAL LETTER TSSE +A692; C; A693; # CYRILLIC CAPITAL LETTER TCHE +A694; C; A695; # CYRILLIC CAPITAL LETTER HWE +A696; C; A697; # CYRILLIC CAPITAL LETTER SHWE +A698; C; A699; # CYRILLIC CAPITAL LETTER DOUBLE O +A69A; C; A69B; # CYRILLIC CAPITAL LETTER CROSSED O +A722; C; A723; # LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF +A724; C; A725; # LATIN CAPITAL LETTER EGYPTOLOGICAL AIN +A726; C; A727; # LATIN CAPITAL LETTER HENG +A728; C; A729; # LATIN CAPITAL LETTER TZ +A72A; C; A72B; # LATIN CAPITAL LETTER TRESILLO +A72C; C; A72D; # LATIN CAPITAL LETTER CUATRILLO +A72E; C; A72F; # LATIN CAPITAL LETTER CUATRILLO WITH COMMA +A732; C; A733; # LATIN CAPITAL LETTER AA +A734; C; A735; # LATIN CAPITAL LETTER AO +A736; C; A737; # LATIN CAPITAL LETTER AU +A738; C; A739; # LATIN CAPITAL LETTER AV +A73A; C; A73B; # LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR +A73C; C; A73D; # LATIN CAPITAL LETTER AY +A73E; C; A73F; # LATIN CAPITAL LETTER REVERSED C WITH DOT +A740; C; A741; # LATIN CAPITAL LETTER K WITH STROKE +A742; C; A743; # LATIN CAPITAL LETTER K WITH DIAGONAL STROKE +A744; C; A745; # LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE +A746; C; A747; # LATIN CAPITAL LETTER BROKEN L +A748; C; A749; # LATIN CAPITAL LETTER L WITH HIGH STROKE +A74A; C; A74B; # LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY +A74C; C; A74D; # LATIN CAPITAL LETTER O WITH LOOP +A74E; C; A74F; # LATIN CAPITAL LETTER OO +A750; C; A751; # LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER +A752; C; A753; # LATIN CAPITAL LETTER P WITH FLOURISH +A754; C; A755; # LATIN CAPITAL LETTER P WITH SQUIRREL TAIL +A756; C; A757; # LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER +A758; C; A759; # LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE +A75A; C; A75B; # LATIN CAPITAL LETTER R ROTUNDA +A75C; C; A75D; # LATIN CAPITAL LETTER RUM ROTUNDA +A75E; C; A75F; # LATIN CAPITAL LETTER V WITH DIAGONAL STROKE +A760; C; A761; # LATIN CAPITAL LETTER VY +A762; C; A763; # LATIN CAPITAL LETTER VISIGOTHIC Z +A764; C; A765; # LATIN CAPITAL LETTER THORN WITH STROKE +A766; C; A767; # LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER +A768; C; A769; # LATIN CAPITAL LETTER VEND +A76A; C; A76B; # LATIN CAPITAL LETTER ET +A76C; C; A76D; # LATIN CAPITAL LETTER IS +A76E; C; A76F; # LATIN CAPITAL LETTER CON +A779; C; A77A; # LATIN CAPITAL LETTER INSULAR D +A77B; C; A77C; # LATIN CAPITAL LETTER INSULAR F +A77D; C; 1D79; # LATIN CAPITAL LETTER INSULAR G +A77E; C; A77F; # LATIN CAPITAL LETTER TURNED INSULAR G +A780; C; A781; # LATIN CAPITAL LETTER TURNED L +A782; C; A783; # LATIN CAPITAL LETTER INSULAR R +A784; C; A785; # LATIN CAPITAL LETTER INSULAR S +A786; C; A787; # LATIN CAPITAL LETTER INSULAR T +A78B; C; A78C; # LATIN CAPITAL LETTER SALTILLO +A78D; C; 0265; # LATIN CAPITAL LETTER TURNED H +A790; C; A791; # LATIN CAPITAL LETTER N WITH DESCENDER +A792; C; A793; # LATIN CAPITAL LETTER C WITH BAR +A796; C; A797; # LATIN CAPITAL LETTER B WITH FLOURISH +A798; C; A799; # LATIN CAPITAL LETTER F WITH STROKE +A79A; C; A79B; # LATIN CAPITAL LETTER VOLAPUK AE +A79C; C; A79D; # LATIN CAPITAL LETTER VOLAPUK OE +A79E; C; A79F; # LATIN CAPITAL LETTER VOLAPUK UE +A7A0; C; A7A1; # LATIN CAPITAL LETTER G WITH OBLIQUE STROKE +A7A2; C; A7A3; # LATIN CAPITAL LETTER K WITH OBLIQUE STROKE +A7A4; C; A7A5; # LATIN CAPITAL LETTER N WITH OBLIQUE STROKE +A7A6; C; A7A7; # LATIN CAPITAL LETTER R WITH OBLIQUE STROKE +A7A8; C; A7A9; # LATIN CAPITAL LETTER S WITH OBLIQUE STROKE +A7AA; C; 0266; # LATIN CAPITAL LETTER H WITH HOOK +A7AB; C; 025C; # LATIN CAPITAL LETTER REVERSED OPEN E +A7AC; C; 0261; # LATIN CAPITAL LETTER SCRIPT G +A7AD; C; 026C; # LATIN CAPITAL LETTER L WITH BELT +A7AE; C; 026A; # LATIN CAPITAL LETTER SMALL CAPITAL I +A7B0; C; 029E; # LATIN CAPITAL LETTER TURNED K +A7B1; C; 0287; # LATIN CAPITAL LETTER TURNED T +A7B2; C; 029D; # LATIN CAPITAL LETTER J WITH CROSSED-TAIL +A7B3; C; AB53; # LATIN CAPITAL LETTER CHI +A7B4; C; A7B5; # LATIN CAPITAL LETTER BETA +A7B6; C; A7B7; # LATIN CAPITAL LETTER OMEGA +A7B8; C; A7B9; # LATIN CAPITAL LETTER U WITH STROKE +A7BA; C; A7BB; # LATIN CAPITAL LETTER GLOTTAL A +A7BC; C; A7BD; # LATIN CAPITAL LETTER GLOTTAL I +A7BE; C; A7BF; # LATIN CAPITAL LETTER GLOTTAL U +A7C0; C; A7C1; # LATIN CAPITAL LETTER OLD POLISH O +A7C2; C; A7C3; # LATIN CAPITAL LETTER ANGLICANA W +A7C4; C; A794; # LATIN CAPITAL LETTER C WITH PALATAL HOOK +A7C5; C; 0282; # LATIN CAPITAL LETTER S WITH HOOK +A7C6; C; 1D8E; # LATIN CAPITAL LETTER Z WITH PALATAL HOOK +A7C7; C; A7C8; # LATIN CAPITAL LETTER D WITH SHORT STROKE OVERLAY +A7C9; C; A7CA; # LATIN CAPITAL LETTER S WITH SHORT STROKE OVERLAY +A7D0; C; A7D1; # LATIN CAPITAL LETTER CLOSED INSULAR G +A7D6; C; A7D7; # LATIN CAPITAL LETTER MIDDLE SCOTS S +A7D8; C; A7D9; # LATIN CAPITAL LETTER SIGMOID S +A7F5; C; A7F6; # LATIN CAPITAL LETTER REVERSED HALF H +AB70; C; 13A0; # CHEROKEE SMALL LETTER A +AB71; C; 13A1; # CHEROKEE SMALL LETTER E +AB72; C; 13A2; # CHEROKEE SMALL LETTER I +AB73; C; 13A3; # CHEROKEE SMALL LETTER O +AB74; C; 13A4; # CHEROKEE SMALL LETTER U +AB75; C; 13A5; # CHEROKEE SMALL LETTER V +AB76; C; 13A6; # CHEROKEE SMALL LETTER GA +AB77; C; 13A7; # CHEROKEE SMALL LETTER KA +AB78; C; 13A8; # CHEROKEE SMALL LETTER GE +AB79; C; 13A9; # CHEROKEE SMALL LETTER GI +AB7A; C; 13AA; # CHEROKEE SMALL LETTER GO +AB7B; C; 13AB; # CHEROKEE SMALL LETTER GU +AB7C; C; 13AC; # CHEROKEE SMALL LETTER GV +AB7D; C; 13AD; # CHEROKEE SMALL LETTER HA +AB7E; C; 13AE; # CHEROKEE SMALL LETTER HE +AB7F; C; 13AF; # CHEROKEE SMALL LETTER HI +AB80; C; 13B0; # CHEROKEE SMALL LETTER HO +AB81; C; 13B1; # CHEROKEE SMALL LETTER HU +AB82; C; 13B2; # CHEROKEE SMALL LETTER HV +AB83; C; 13B3; # CHEROKEE SMALL LETTER LA +AB84; C; 13B4; # CHEROKEE SMALL LETTER LE +AB85; C; 13B5; # CHEROKEE SMALL LETTER LI +AB86; C; 13B6; # CHEROKEE SMALL LETTER LO +AB87; C; 13B7; # CHEROKEE SMALL LETTER LU +AB88; C; 13B8; # CHEROKEE SMALL LETTER LV +AB89; C; 13B9; # CHEROKEE SMALL LETTER MA +AB8A; C; 13BA; # CHEROKEE SMALL LETTER ME +AB8B; C; 13BB; # CHEROKEE SMALL LETTER MI +AB8C; C; 13BC; # CHEROKEE SMALL LETTER MO +AB8D; C; 13BD; # CHEROKEE SMALL LETTER MU +AB8E; C; 13BE; # CHEROKEE SMALL LETTER NA +AB8F; C; 13BF; # CHEROKEE SMALL LETTER HNA +AB90; C; 13C0; # CHEROKEE SMALL LETTER NAH +AB91; C; 13C1; # CHEROKEE SMALL LETTER NE +AB92; C; 13C2; # CHEROKEE SMALL LETTER NI +AB93; C; 13C3; # CHEROKEE SMALL LETTER NO +AB94; C; 13C4; # CHEROKEE SMALL LETTER NU +AB95; C; 13C5; # CHEROKEE SMALL LETTER NV +AB96; C; 13C6; # CHEROKEE SMALL LETTER QUA +AB97; C; 13C7; # CHEROKEE SMALL LETTER QUE +AB98; C; 13C8; # CHEROKEE SMALL LETTER QUI +AB99; C; 13C9; # CHEROKEE SMALL LETTER QUO +AB9A; C; 13CA; # CHEROKEE SMALL LETTER QUU +AB9B; C; 13CB; # CHEROKEE SMALL LETTER QUV +AB9C; C; 13CC; # CHEROKEE SMALL LETTER SA +AB9D; C; 13CD; # CHEROKEE SMALL LETTER S +AB9E; C; 13CE; # CHEROKEE SMALL LETTER SE +AB9F; C; 13CF; # CHEROKEE SMALL LETTER SI +ABA0; C; 13D0; # CHEROKEE SMALL LETTER SO +ABA1; C; 13D1; # CHEROKEE SMALL LETTER SU +ABA2; C; 13D2; # CHEROKEE SMALL LETTER SV +ABA3; C; 13D3; # CHEROKEE SMALL LETTER DA +ABA4; C; 13D4; # CHEROKEE SMALL LETTER TA +ABA5; C; 13D5; # CHEROKEE SMALL LETTER DE +ABA6; C; 13D6; # CHEROKEE SMALL LETTER TE +ABA7; C; 13D7; # CHEROKEE SMALL LETTER DI +ABA8; C; 13D8; # CHEROKEE SMALL LETTER TI +ABA9; C; 13D9; # CHEROKEE SMALL LETTER DO +ABAA; C; 13DA; # CHEROKEE SMALL LETTER DU +ABAB; C; 13DB; # CHEROKEE SMALL LETTER DV +ABAC; C; 13DC; # CHEROKEE SMALL LETTER DLA +ABAD; C; 13DD; # CHEROKEE SMALL LETTER TLA +ABAE; C; 13DE; # CHEROKEE SMALL LETTER TLE +ABAF; C; 13DF; # CHEROKEE SMALL LETTER TLI +ABB0; C; 13E0; # CHEROKEE SMALL LETTER TLO +ABB1; C; 13E1; # CHEROKEE SMALL LETTER TLU +ABB2; C; 13E2; # CHEROKEE SMALL LETTER TLV +ABB3; C; 13E3; # CHEROKEE SMALL LETTER TSA +ABB4; C; 13E4; # CHEROKEE SMALL LETTER TSE +ABB5; C; 13E5; # CHEROKEE SMALL LETTER TSI +ABB6; C; 13E6; # CHEROKEE SMALL LETTER TSO +ABB7; C; 13E7; # CHEROKEE SMALL LETTER TSU +ABB8; C; 13E8; # CHEROKEE SMALL LETTER TSV +ABB9; C; 13E9; # CHEROKEE SMALL LETTER WA +ABBA; C; 13EA; # CHEROKEE SMALL LETTER WE +ABBB; C; 13EB; # CHEROKEE SMALL LETTER WI +ABBC; C; 13EC; # CHEROKEE SMALL LETTER WO +ABBD; C; 13ED; # CHEROKEE SMALL LETTER WU +ABBE; C; 13EE; # CHEROKEE SMALL LETTER WV +ABBF; C; 13EF; # CHEROKEE SMALL LETTER YA +FB00; F; 0066 0066; # LATIN SMALL LIGATURE FF +FB01; F; 0066 0069; # LATIN SMALL LIGATURE FI +FB02; F; 0066 006C; # LATIN SMALL LIGATURE FL +FB03; F; 0066 0066 0069; # LATIN SMALL LIGATURE FFI +FB04; F; 0066 0066 006C; # LATIN SMALL LIGATURE FFL +FB05; F; 0073 0074; # LATIN SMALL LIGATURE LONG S T +FB06; F; 0073 0074; # LATIN SMALL LIGATURE ST +FB13; F; 0574 0576; # ARMENIAN SMALL LIGATURE MEN NOW +FB14; F; 0574 0565; # ARMENIAN SMALL LIGATURE MEN ECH +FB15; F; 0574 056B; # ARMENIAN SMALL LIGATURE MEN INI +FB16; F; 057E 0576; # ARMENIAN SMALL LIGATURE VEW NOW +FB17; F; 0574 056D; # ARMENIAN SMALL LIGATURE MEN XEH +FF21; C; FF41; # FULLWIDTH LATIN CAPITAL LETTER A +FF22; C; FF42; # FULLWIDTH LATIN CAPITAL LETTER B +FF23; C; FF43; # FULLWIDTH LATIN CAPITAL LETTER C +FF24; C; FF44; # FULLWIDTH LATIN CAPITAL LETTER D +FF25; C; FF45; # FULLWIDTH LATIN CAPITAL LETTER E +FF26; C; FF46; # FULLWIDTH LATIN CAPITAL LETTER F +FF27; C; FF47; # FULLWIDTH LATIN CAPITAL LETTER G +FF28; C; FF48; # FULLWIDTH LATIN CAPITAL LETTER H +FF29; C; FF49; # FULLWIDTH LATIN CAPITAL LETTER I +FF2A; C; FF4A; # FULLWIDTH LATIN CAPITAL LETTER J +FF2B; C; FF4B; # FULLWIDTH LATIN CAPITAL LETTER K +FF2C; C; FF4C; # FULLWIDTH LATIN CAPITAL LETTER L +FF2D; C; FF4D; # FULLWIDTH LATIN CAPITAL LETTER M +FF2E; C; FF4E; # FULLWIDTH LATIN CAPITAL LETTER N +FF2F; C; FF4F; # FULLWIDTH LATIN CAPITAL LETTER O +FF30; C; FF50; # FULLWIDTH LATIN CAPITAL LETTER P +FF31; C; FF51; # FULLWIDTH LATIN CAPITAL LETTER Q +FF32; C; FF52; # FULLWIDTH LATIN CAPITAL LETTER R +FF33; C; FF53; # FULLWIDTH LATIN CAPITAL LETTER S +FF34; C; FF54; # FULLWIDTH LATIN CAPITAL LETTER T +FF35; C; FF55; # FULLWIDTH LATIN CAPITAL LETTER U +FF36; C; FF56; # FULLWIDTH LATIN CAPITAL LETTER V +FF37; C; FF57; # FULLWIDTH LATIN CAPITAL LETTER W +FF38; C; FF58; # FULLWIDTH LATIN CAPITAL LETTER X +FF39; C; FF59; # FULLWIDTH LATIN CAPITAL LETTER Y +FF3A; C; FF5A; # FULLWIDTH LATIN CAPITAL LETTER Z +10400; C; 10428; # DESERET CAPITAL LETTER LONG I +10401; C; 10429; # DESERET CAPITAL LETTER LONG E +10402; C; 1042A; # DESERET CAPITAL LETTER LONG A +10403; C; 1042B; # DESERET CAPITAL LETTER LONG AH +10404; C; 1042C; # DESERET CAPITAL LETTER LONG O +10405; C; 1042D; # DESERET CAPITAL LETTER LONG OO +10406; C; 1042E; # DESERET CAPITAL LETTER SHORT I +10407; C; 1042F; # DESERET CAPITAL LETTER SHORT E +10408; C; 10430; # DESERET CAPITAL LETTER SHORT A +10409; C; 10431; # DESERET CAPITAL LETTER SHORT AH +1040A; C; 10432; # DESERET CAPITAL LETTER SHORT O +1040B; C; 10433; # DESERET CAPITAL LETTER SHORT OO +1040C; C; 10434; # DESERET CAPITAL LETTER AY +1040D; C; 10435; # DESERET CAPITAL LETTER OW +1040E; C; 10436; # DESERET CAPITAL LETTER WU +1040F; C; 10437; # DESERET CAPITAL LETTER YEE +10410; C; 10438; # DESERET CAPITAL LETTER H +10411; C; 10439; # DESERET CAPITAL LETTER PEE +10412; C; 1043A; # DESERET CAPITAL LETTER BEE +10413; C; 1043B; # DESERET CAPITAL LETTER TEE +10414; C; 1043C; # DESERET CAPITAL LETTER DEE +10415; C; 1043D; # DESERET CAPITAL LETTER CHEE +10416; C; 1043E; # DESERET CAPITAL LETTER JEE +10417; C; 1043F; # DESERET CAPITAL LETTER KAY +10418; C; 10440; # DESERET CAPITAL LETTER GAY +10419; C; 10441; # DESERET CAPITAL LETTER EF +1041A; C; 10442; # DESERET CAPITAL LETTER VEE +1041B; C; 10443; # DESERET CAPITAL LETTER ETH +1041C; C; 10444; # DESERET CAPITAL LETTER THEE +1041D; C; 10445; # DESERET CAPITAL LETTER ES +1041E; C; 10446; # DESERET CAPITAL LETTER ZEE +1041F; C; 10447; # DESERET CAPITAL LETTER ESH +10420; C; 10448; # DESERET CAPITAL LETTER ZHEE +10421; C; 10449; # DESERET CAPITAL LETTER ER +10422; C; 1044A; # DESERET CAPITAL LETTER EL +10423; C; 1044B; # DESERET CAPITAL LETTER EM +10424; C; 1044C; # DESERET CAPITAL LETTER EN +10425; C; 1044D; # DESERET CAPITAL LETTER ENG +10426; C; 1044E; # DESERET CAPITAL LETTER OI +10427; C; 1044F; # DESERET CAPITAL LETTER EW +104B0; C; 104D8; # OSAGE CAPITAL LETTER A +104B1; C; 104D9; # OSAGE CAPITAL LETTER AI +104B2; C; 104DA; # OSAGE CAPITAL LETTER AIN +104B3; C; 104DB; # OSAGE CAPITAL LETTER AH +104B4; C; 104DC; # OSAGE CAPITAL LETTER BRA +104B5; C; 104DD; # OSAGE CAPITAL LETTER CHA +104B6; C; 104DE; # OSAGE CAPITAL LETTER EHCHA +104B7; C; 104DF; # OSAGE CAPITAL LETTER E +104B8; C; 104E0; # OSAGE CAPITAL LETTER EIN +104B9; C; 104E1; # OSAGE CAPITAL LETTER HA +104BA; C; 104E2; # OSAGE CAPITAL LETTER HYA +104BB; C; 104E3; # OSAGE CAPITAL LETTER I +104BC; C; 104E4; # OSAGE CAPITAL LETTER KA +104BD; C; 104E5; # OSAGE CAPITAL LETTER EHKA +104BE; C; 104E6; # OSAGE CAPITAL LETTER KYA +104BF; C; 104E7; # OSAGE CAPITAL LETTER LA +104C0; C; 104E8; # OSAGE CAPITAL LETTER MA +104C1; C; 104E9; # OSAGE CAPITAL LETTER NA +104C2; C; 104EA; # OSAGE CAPITAL LETTER O +104C3; C; 104EB; # OSAGE CAPITAL LETTER OIN +104C4; C; 104EC; # OSAGE CAPITAL LETTER PA +104C5; C; 104ED; # OSAGE CAPITAL LETTER EHPA +104C6; C; 104EE; # OSAGE CAPITAL LETTER SA +104C7; C; 104EF; # OSAGE CAPITAL LETTER SHA +104C8; C; 104F0; # OSAGE CAPITAL LETTER TA +104C9; C; 104F1; # OSAGE CAPITAL LETTER EHTA +104CA; C; 104F2; # OSAGE CAPITAL LETTER TSA +104CB; C; 104F3; # OSAGE CAPITAL LETTER EHTSA +104CC; C; 104F4; # OSAGE CAPITAL LETTER TSHA +104CD; C; 104F5; # OSAGE CAPITAL LETTER DHA +104CE; C; 104F6; # OSAGE CAPITAL LETTER U +104CF; C; 104F7; # OSAGE CAPITAL LETTER WA +104D0; C; 104F8; # OSAGE CAPITAL LETTER KHA +104D1; C; 104F9; # OSAGE CAPITAL LETTER GHA +104D2; C; 104FA; # OSAGE CAPITAL LETTER ZA +104D3; C; 104FB; # OSAGE CAPITAL LETTER ZHA +10570; C; 10597; # VITHKUQI CAPITAL LETTER A +10571; C; 10598; # VITHKUQI CAPITAL LETTER BBE +10572; C; 10599; # VITHKUQI CAPITAL LETTER BE +10573; C; 1059A; # VITHKUQI CAPITAL LETTER CE +10574; C; 1059B; # VITHKUQI CAPITAL LETTER CHE +10575; C; 1059C; # VITHKUQI CAPITAL LETTER DE +10576; C; 1059D; # VITHKUQI CAPITAL LETTER DHE +10577; C; 1059E; # VITHKUQI CAPITAL LETTER EI +10578; C; 1059F; # VITHKUQI CAPITAL LETTER E +10579; C; 105A0; # VITHKUQI CAPITAL LETTER FE +1057A; C; 105A1; # VITHKUQI CAPITAL LETTER GA +1057C; C; 105A3; # VITHKUQI CAPITAL LETTER HA +1057D; C; 105A4; # VITHKUQI CAPITAL LETTER HHA +1057E; C; 105A5; # VITHKUQI CAPITAL LETTER I +1057F; C; 105A6; # VITHKUQI CAPITAL LETTER IJE +10580; C; 105A7; # VITHKUQI CAPITAL LETTER JE +10581; C; 105A8; # VITHKUQI CAPITAL LETTER KA +10582; C; 105A9; # VITHKUQI CAPITAL LETTER LA +10583; C; 105AA; # VITHKUQI CAPITAL LETTER LLA +10584; C; 105AB; # VITHKUQI CAPITAL LETTER ME +10585; C; 105AC; # VITHKUQI CAPITAL LETTER NE +10586; C; 105AD; # VITHKUQI CAPITAL LETTER NJE +10587; C; 105AE; # VITHKUQI CAPITAL LETTER O +10588; C; 105AF; # VITHKUQI CAPITAL LETTER PE +10589; C; 105B0; # VITHKUQI CAPITAL LETTER QA +1058A; C; 105B1; # VITHKUQI CAPITAL LETTER RE +1058C; C; 105B3; # VITHKUQI CAPITAL LETTER SE +1058D; C; 105B4; # VITHKUQI CAPITAL LETTER SHE +1058E; C; 105B5; # VITHKUQI CAPITAL LETTER TE +1058F; C; 105B6; # VITHKUQI CAPITAL LETTER THE +10590; C; 105B7; # VITHKUQI CAPITAL LETTER U +10591; C; 105B8; # VITHKUQI CAPITAL LETTER VE +10592; C; 105B9; # VITHKUQI CAPITAL LETTER XE +10594; C; 105BB; # VITHKUQI CAPITAL LETTER Y +10595; C; 105BC; # VITHKUQI CAPITAL LETTER ZE +10C80; C; 10CC0; # OLD HUNGARIAN CAPITAL LETTER A +10C81; C; 10CC1; # OLD HUNGARIAN CAPITAL LETTER AA +10C82; C; 10CC2; # OLD HUNGARIAN CAPITAL LETTER EB +10C83; C; 10CC3; # OLD HUNGARIAN CAPITAL LETTER AMB +10C84; C; 10CC4; # OLD HUNGARIAN CAPITAL LETTER EC +10C85; C; 10CC5; # OLD HUNGARIAN CAPITAL LETTER ENC +10C86; C; 10CC6; # OLD HUNGARIAN CAPITAL LETTER ECS +10C87; C; 10CC7; # OLD HUNGARIAN CAPITAL LETTER ED +10C88; C; 10CC8; # OLD HUNGARIAN CAPITAL LETTER AND +10C89; C; 10CC9; # OLD HUNGARIAN CAPITAL LETTER E +10C8A; C; 10CCA; # OLD HUNGARIAN CAPITAL LETTER CLOSE E +10C8B; C; 10CCB; # OLD HUNGARIAN CAPITAL LETTER EE +10C8C; C; 10CCC; # OLD HUNGARIAN CAPITAL LETTER EF +10C8D; C; 10CCD; # OLD HUNGARIAN CAPITAL LETTER EG +10C8E; C; 10CCE; # OLD HUNGARIAN CAPITAL LETTER EGY +10C8F; C; 10CCF; # OLD HUNGARIAN CAPITAL LETTER EH +10C90; C; 10CD0; # OLD HUNGARIAN CAPITAL LETTER I +10C91; C; 10CD1; # OLD HUNGARIAN CAPITAL LETTER II +10C92; C; 10CD2; # OLD HUNGARIAN CAPITAL LETTER EJ +10C93; C; 10CD3; # OLD HUNGARIAN CAPITAL LETTER EK +10C94; C; 10CD4; # OLD HUNGARIAN CAPITAL LETTER AK +10C95; C; 10CD5; # OLD HUNGARIAN CAPITAL LETTER UNK +10C96; C; 10CD6; # OLD HUNGARIAN CAPITAL LETTER EL +10C97; C; 10CD7; # OLD HUNGARIAN CAPITAL LETTER ELY +10C98; C; 10CD8; # OLD HUNGARIAN CAPITAL LETTER EM +10C99; C; 10CD9; # OLD HUNGARIAN CAPITAL LETTER EN +10C9A; C; 10CDA; # OLD HUNGARIAN CAPITAL LETTER ENY +10C9B; C; 10CDB; # OLD HUNGARIAN CAPITAL LETTER O +10C9C; C; 10CDC; # OLD HUNGARIAN CAPITAL LETTER OO +10C9D; C; 10CDD; # OLD HUNGARIAN CAPITAL LETTER NIKOLSBURG OE +10C9E; C; 10CDE; # OLD HUNGARIAN CAPITAL LETTER RUDIMENTA OE +10C9F; C; 10CDF; # OLD HUNGARIAN CAPITAL LETTER OEE +10CA0; C; 10CE0; # OLD HUNGARIAN CAPITAL LETTER EP +10CA1; C; 10CE1; # OLD HUNGARIAN CAPITAL LETTER EMP +10CA2; C; 10CE2; # OLD HUNGARIAN CAPITAL LETTER ER +10CA3; C; 10CE3; # OLD HUNGARIAN CAPITAL LETTER SHORT ER +10CA4; C; 10CE4; # OLD HUNGARIAN CAPITAL LETTER ES +10CA5; C; 10CE5; # OLD HUNGARIAN CAPITAL LETTER ESZ +10CA6; C; 10CE6; # OLD HUNGARIAN CAPITAL LETTER ET +10CA7; C; 10CE7; # OLD HUNGARIAN CAPITAL LETTER ENT +10CA8; C; 10CE8; # OLD HUNGARIAN CAPITAL LETTER ETY +10CA9; C; 10CE9; # OLD HUNGARIAN CAPITAL LETTER ECH +10CAA; C; 10CEA; # OLD HUNGARIAN CAPITAL LETTER U +10CAB; C; 10CEB; # OLD HUNGARIAN CAPITAL LETTER UU +10CAC; C; 10CEC; # OLD HUNGARIAN CAPITAL LETTER NIKOLSBURG UE +10CAD; C; 10CED; # OLD HUNGARIAN CAPITAL LETTER RUDIMENTA UE +10CAE; C; 10CEE; # OLD HUNGARIAN CAPITAL LETTER EV +10CAF; C; 10CEF; # OLD HUNGARIAN CAPITAL LETTER EZ +10CB0; C; 10CF0; # OLD HUNGARIAN CAPITAL LETTER EZS +10CB1; C; 10CF1; # OLD HUNGARIAN CAPITAL LETTER ENT-SHAPED SIGN +10CB2; C; 10CF2; # OLD HUNGARIAN CAPITAL LETTER US +118A0; C; 118C0; # WARANG CITI CAPITAL LETTER NGAA +118A1; C; 118C1; # WARANG CITI CAPITAL LETTER A +118A2; C; 118C2; # WARANG CITI CAPITAL LETTER WI +118A3; C; 118C3; # WARANG CITI CAPITAL LETTER YU +118A4; C; 118C4; # WARANG CITI CAPITAL LETTER YA +118A5; C; 118C5; # WARANG CITI CAPITAL LETTER YO +118A6; C; 118C6; # WARANG CITI CAPITAL LETTER II +118A7; C; 118C7; # WARANG CITI CAPITAL LETTER UU +118A8; C; 118C8; # WARANG CITI CAPITAL LETTER E +118A9; C; 118C9; # WARANG CITI CAPITAL LETTER O +118AA; C; 118CA; # WARANG CITI CAPITAL LETTER ANG +118AB; C; 118CB; # WARANG CITI CAPITAL LETTER GA +118AC; C; 118CC; # WARANG CITI CAPITAL LETTER KO +118AD; C; 118CD; # WARANG CITI CAPITAL LETTER ENY +118AE; C; 118CE; # WARANG CITI CAPITAL LETTER YUJ +118AF; C; 118CF; # WARANG CITI CAPITAL LETTER UC +118B0; C; 118D0; # WARANG CITI CAPITAL LETTER ENN +118B1; C; 118D1; # WARANG CITI CAPITAL LETTER ODD +118B2; C; 118D2; # WARANG CITI CAPITAL LETTER TTE +118B3; C; 118D3; # WARANG CITI CAPITAL LETTER NUNG +118B4; C; 118D4; # WARANG CITI CAPITAL LETTER DA +118B5; C; 118D5; # WARANG CITI CAPITAL LETTER AT +118B6; C; 118D6; # WARANG CITI CAPITAL LETTER AM +118B7; C; 118D7; # WARANG CITI CAPITAL LETTER BU +118B8; C; 118D8; # WARANG CITI CAPITAL LETTER PU +118B9; C; 118D9; # WARANG CITI CAPITAL LETTER HIYO +118BA; C; 118DA; # WARANG CITI CAPITAL LETTER HOLO +118BB; C; 118DB; # WARANG CITI CAPITAL LETTER HORR +118BC; C; 118DC; # WARANG CITI CAPITAL LETTER HAR +118BD; C; 118DD; # WARANG CITI CAPITAL LETTER SSUU +118BE; C; 118DE; # WARANG CITI CAPITAL LETTER SII +118BF; C; 118DF; # WARANG CITI CAPITAL LETTER VIYO +16E40; C; 16E60; # MEDEFAIDRIN CAPITAL LETTER M +16E41; C; 16E61; # MEDEFAIDRIN CAPITAL LETTER S +16E42; C; 16E62; # MEDEFAIDRIN CAPITAL LETTER V +16E43; C; 16E63; # MEDEFAIDRIN CAPITAL LETTER W +16E44; C; 16E64; # MEDEFAIDRIN CAPITAL LETTER ATIU +16E45; C; 16E65; # MEDEFAIDRIN CAPITAL LETTER Z +16E46; C; 16E66; # MEDEFAIDRIN CAPITAL LETTER KP +16E47; C; 16E67; # MEDEFAIDRIN CAPITAL LETTER P +16E48; C; 16E68; # MEDEFAIDRIN CAPITAL LETTER T +16E49; C; 16E69; # MEDEFAIDRIN CAPITAL LETTER G +16E4A; C; 16E6A; # MEDEFAIDRIN CAPITAL LETTER F +16E4B; C; 16E6B; # MEDEFAIDRIN CAPITAL LETTER I +16E4C; C; 16E6C; # MEDEFAIDRIN CAPITAL LETTER K +16E4D; C; 16E6D; # MEDEFAIDRIN CAPITAL LETTER A +16E4E; C; 16E6E; # MEDEFAIDRIN CAPITAL LETTER J +16E4F; C; 16E6F; # MEDEFAIDRIN CAPITAL LETTER E +16E50; C; 16E70; # MEDEFAIDRIN CAPITAL LETTER B +16E51; C; 16E71; # MEDEFAIDRIN CAPITAL LETTER C +16E52; C; 16E72; # MEDEFAIDRIN CAPITAL LETTER U +16E53; C; 16E73; # MEDEFAIDRIN CAPITAL LETTER YU +16E54; C; 16E74; # MEDEFAIDRIN CAPITAL LETTER L +16E55; C; 16E75; # MEDEFAIDRIN CAPITAL LETTER Q +16E56; C; 16E76; # MEDEFAIDRIN CAPITAL LETTER HP +16E57; C; 16E77; # MEDEFAIDRIN CAPITAL LETTER NY +16E58; C; 16E78; # MEDEFAIDRIN CAPITAL LETTER X +16E59; C; 16E79; # MEDEFAIDRIN CAPITAL LETTER D +16E5A; C; 16E7A; # MEDEFAIDRIN CAPITAL LETTER OE +16E5B; C; 16E7B; # MEDEFAIDRIN CAPITAL LETTER N +16E5C; C; 16E7C; # MEDEFAIDRIN CAPITAL LETTER R +16E5D; C; 16E7D; # MEDEFAIDRIN CAPITAL LETTER O +16E5E; C; 16E7E; # MEDEFAIDRIN CAPITAL LETTER AI +16E5F; C; 16E7F; # MEDEFAIDRIN CAPITAL LETTER Y +1E900; C; 1E922; # ADLAM CAPITAL LETTER ALIF +1E901; C; 1E923; # ADLAM CAPITAL LETTER DAALI +1E902; C; 1E924; # ADLAM CAPITAL LETTER LAAM +1E903; C; 1E925; # ADLAM CAPITAL LETTER MIIM +1E904; C; 1E926; # ADLAM CAPITAL LETTER BA +1E905; C; 1E927; # ADLAM CAPITAL LETTER SINNYIIYHE +1E906; C; 1E928; # ADLAM CAPITAL LETTER PE +1E907; C; 1E929; # ADLAM CAPITAL LETTER BHE +1E908; C; 1E92A; # ADLAM CAPITAL LETTER RA +1E909; C; 1E92B; # ADLAM CAPITAL LETTER E +1E90A; C; 1E92C; # ADLAM CAPITAL LETTER FA +1E90B; C; 1E92D; # ADLAM CAPITAL LETTER I +1E90C; C; 1E92E; # ADLAM CAPITAL LETTER O +1E90D; C; 1E92F; # ADLAM CAPITAL LETTER DHA +1E90E; C; 1E930; # ADLAM CAPITAL LETTER YHE +1E90F; C; 1E931; # ADLAM CAPITAL LETTER WAW +1E910; C; 1E932; # ADLAM CAPITAL LETTER NUN +1E911; C; 1E933; # ADLAM CAPITAL LETTER KAF +1E912; C; 1E934; # ADLAM CAPITAL LETTER YA +1E913; C; 1E935; # ADLAM CAPITAL LETTER U +1E914; C; 1E936; # ADLAM CAPITAL LETTER JIIM +1E915; C; 1E937; # ADLAM CAPITAL LETTER CHI +1E916; C; 1E938; # ADLAM CAPITAL LETTER HA +1E917; C; 1E939; # ADLAM CAPITAL LETTER QAAF +1E918; C; 1E93A; # ADLAM CAPITAL LETTER GA +1E919; C; 1E93B; # ADLAM CAPITAL LETTER NYA +1E91A; C; 1E93C; # ADLAM CAPITAL LETTER TU +1E91B; C; 1E93D; # ADLAM CAPITAL LETTER NHA +1E91C; C; 1E93E; # ADLAM CAPITAL LETTER VA +1E91D; C; 1E93F; # ADLAM CAPITAL LETTER KHA +1E91E; C; 1E940; # ADLAM CAPITAL LETTER GBE +1E91F; C; 1E941; # ADLAM CAPITAL LETTER ZAL +1E920; C; 1E942; # ADLAM CAPITAL LETTER KPO +1E921; C; 1E943; # ADLAM CAPITAL LETTER SHA +# +# EOF diff --git a/packages/skill-tree/fixtures/vectors/identity-v1-vectors.json b/packages/skill-tree/fixtures/vectors/identity-v1-vectors.json new file mode 100644 index 0000000..d532820 --- /dev/null +++ b/packages/skill-tree/fixtures/vectors/identity-v1-vectors.json @@ -0,0 +1,782 @@ +{ + "schema": "agentkit.identity-vectors/v1", + "spec": "docs/specs/skill-lifecycle-contract.md#2-identity", + "pattern": "^@[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$", + "notes": [ + "Each identity segment must also pass the canonical-tree segment rules and be at most 64 bytes.", + "kitMapping rows without an error give the identity and portable alias of a kit skill directory.", + "provenance rows give the trust recorded for an accepted identity, or the rejection code. An unknown provider, a kit row without a known origin, an invalid identity, or a kit row whose identity is not @agentkit fails with identity.invalid; these checks run before the reserved-publisher check. verified on a skillx row says whether the release provenance verified.", + "aliases rows are invocation aliases. An alias is 1 to 128 characters from [A-Za-z0-9._:/$-]; '$' may appear anywhere. A leading '/' is allowed only in the native slash form '/:', a single segment after the '/'. Split on '/', the alias has no empty, '.' or '..' segment, so no trailing '/'; every ':' in a segment has non-empty text on both sides; and every segment, and every ':'-separated part of one, holds at least one ASCII letter or digit, so '$', '$$' and '/$:x' are invalid. Any other alias, including a non-ASCII one, fails with identity.invalid.", + "Reserved prefixes: after removing every leading '/' of the alias and every leading '$' of each '/'-separated segment, if any segment starts with ak- or ak:, compared after mapping only ASCII A-Z to a-z, the alias is reserved for @agentkit identities; any other identity fails with identity.reserved_alias. Shape errors are checked first.", + "directoryAliases rows are projection directory names (the portable runtime alias): an alias that is one segment, passes the canonical-tree segment rules (so it never starts with .ak-tmp.) and contains no ':' or '$'; otherwise identity.invalid, then the reserved-prefix check. groupKey is the alias with only ASCII A-Z mapped to a-z; collision and co-ownership grouping use it on every host. directoryAliasCollisions pairs collide exactly when their groupKeys are equal.", + "exposure rows apply to every reader that derives exposure, SkillX included. They give either frontmatter, the YAML text between the delimiter lines, or file, a whole SKILL.md, and the derived exposure. Frontmatter is the block between a first line '---' and the next '---' line, after removing a UTF-8 BOM, a CR before each LF, and trailing spaces or tabs on the delimiter lines. Only top-level plain YAML scalars true, True, TRUE, false, False and FALSE are booleans; quoted strings, yes, no, on, off, 1 and other spellings count as absent.", + "A parse failure is a YAML 1.2 syntax error. A duplicate key other than the two exposure keys is not one (the TypeScript yaml package needs uniqueKeys: false).", + "warning is true when the frontmatter is unterminated or fails to parse, when it holds a YAML value that is not a mapping (a scalar, a sequence or null, such as '~'), or when either key is duplicated, carries an explicit tag, or uses an anchor or alias; exposure is then AUTO. Frontmatter that holds no YAML document at all, because it is empty or only comments, is AUTO without a warning." + ], + "kitMapping": [ + { + "kit": "core", + "dir": "ak-plan", + "identity": "@agentkit/plan", + "portableAlias": "ak-plan" + }, + { + "kit": "core", + "dir": "ak-test", + "identity": "@agentkit/test", + "portableAlias": "ak-test" + }, + { + "kit": "engineer", + "dir": "ak-code-review", + "identity": "@agentkit/code-review", + "portableAlias": "ak-code-review" + }, + { + "kit": "marketing", + "dir": "ak-youtube-thumbnail-design", + "identity": "@agentkit/youtube-thumbnail-design", + "portableAlias": "ak-youtube-thumbnail-design" + }, + { + "kit": "core", + "dir": "plan", + "error": "identity.invalid" + }, + { + "kit": "core", + "dir": "ak-", + "error": "identity.invalid" + }, + { + "kit": "core", + "dir": "ak-Plan", + "error": "identity.invalid" + }, + { + "kit": "core", + "dir": "ak-con", + "error": "identity.invalid" + }, + { + "kit": "core", + "dir": "ak-notes.", + "error": "identity.invalid" + } + ], + "valid": [ + "@agentkit/plan", + "@acme/my_skill.v2", + "@a/b", + "@0pub/0-x", + "@pub/a-", + "@pub/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "invalid": [ + { + "value": "agentkit/plan", + "error": "identity.invalid" + }, + { + "value": "@Agentkit/plan", + "error": "identity.invalid" + }, + { + "value": "@agentkit/Plan", + "error": "identity.invalid" + }, + { + "value": "@-pub/x", + "error": "identity.invalid" + }, + { + "value": "@pub/", + "error": "identity.invalid" + }, + { + "value": "@pub/a/b", + "error": "identity.invalid" + }, + { + "value": "@pub/a b", + "error": "identity.invalid" + }, + { + "value": "ak:plan", + "error": "identity.invalid" + }, + { + "value": "@pub/con", + "error": "identity.invalid" + }, + { + "value": "@pub/aux.x", + "error": "identity.invalid" + }, + { + "value": "@con/x", + "error": "identity.invalid" + }, + { + "value": "@pub/a.", + "error": "identity.invalid" + }, + { + "value": "@pub/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "error": "identity.invalid" + }, + { + "value": "@ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp/x", + "error": "identity.invalid" + } + ], + "provenance": [ + { + "identity": "@agentkit/plan", + "provider": "kit", + "origin": "remote-signed", + "expect": { + "trust": "signed-kit" + } + }, + { + "identity": "@agentkit/plan", + "provider": "kit", + "origin": "local-dev", + "expect": { + "trust": "local-unverified" + } + }, + { + "identity": "@agentkit/plan", + "provider": "kit", + "origin": "remote-signed", + "overlay": true, + "expect": { + "trust": "local-unverified" + } + }, + { + "identity": "@agentkit/plan", + "provider": "kit", + "origin": "mirror", + "expect": { + "error": "identity.invalid" + } + }, + { + "identity": "@agentkit/plan", + "provider": "kit", + "expect": { + "error": "identity.invalid" + } + }, + { + "identity": "@agentkit/plan", + "provider": "skillx", + "verified": true, + "expect": { + "error": "identity.reserved_publisher" + } + }, + { + "identity": "@agentkit/plan", + "provider": "local", + "expect": { + "error": "identity.reserved_publisher" + } + }, + { + "identity": "@agentkit/plan", + "provider": "git", + "expect": { + "error": "identity.reserved_publisher" + } + }, + { + "identity": "@acme/plan", + "provider": "skillx", + "verified": true, + "expect": { + "trust": "publisher-signed" + } + }, + { + "identity": "@acme/plan", + "provider": "skillx", + "verified": false, + "expect": { + "trust": "local-unverified" + } + }, + { + "identity": "@acme/plan", + "provider": "local", + "expect": { + "trust": "local-unverified" + } + }, + { + "identity": "@acme/plan", + "provider": "git", + "expect": { + "trust": "local-unverified" + } + }, + { + "identity": "@acme/plan", + "provider": "npm", + "expect": { + "error": "identity.invalid" + } + }, + { + "identity": "@agentkit/plan", + "provider": "npm", + "expect": { + "error": "identity.invalid" + } + }, + { + "identity": "@acme/plan", + "provider": "kit", + "origin": "remote-signed", + "expect": { + "error": "identity.invalid" + } + }, + { + "identity": "@agentkit/Plan", + "provider": "kit", + "origin": "remote-signed", + "expect": { + "error": "identity.invalid" + } + } + ], + "aliases": [ + { + "identity": "@agentkit/plan", + "alias": "ak-plan", + "error": null + }, + { + "identity": "@agentkit/plan", + "alias": "ak:plan", + "error": null + }, + { + "identity": "@evil/plan", + "alias": "ak-plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@evil/plan", + "alias": "ak:plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@evil/plan", + "alias": "AK-plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@acme/plan", + "alias": "plan", + "error": null + }, + { + "identity": "@acme/plan", + "alias": "acme-plan", + "error": null + }, + { + "identity": "@evil/plan", + "alias": "aK-plan", + "error": "identity.invalid" + }, + { + "identity": "@evil/plan", + "alias": "аk-plan", + "error": "identity.invalid" + }, + { + "identity": "@agentkit/plan", + "alias": "aK-plan", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp", + "error": null + }, + { + "identity": "@acme/plan", + "alias": "ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "my plan", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "acme/plan", + "error": null + }, + { + "identity": "@acme/plan", + "alias": "a//b", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "./plan", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "acme/.", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "acme/..", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "acme/", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "/acme-plan", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "//plan", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "/acme:plan", + "error": null + }, + { + "identity": "@acme/plan", + "alias": "/acme/x:plan", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "a$b", + "error": null + }, + { + "identity": "@acme/plan", + "alias": "plan!", + "error": "identity.invalid" + }, + { + "identity": "@agentkit/plan", + "alias": "/ak:plan", + "error": null + }, + { + "identity": "@evil/plan", + "alias": "/ak:plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@agentkit/plan", + "alias": "$ak:plan", + "error": null + }, + { + "identity": "@evil/plan", + "alias": "$AK:plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@evil/plan", + "alias": "$$ak:plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@evil/plan", + "alias": "/$ak:plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@evil/plan", + "alias": "$/ak:plan", + "error": "identity.invalid" + }, + { + "identity": "@evil/plan", + "alias": "core/ak-plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@agentkit/plan", + "alias": "core/ak-plan", + "error": null + }, + { + "identity": "@acme/plan", + "alias": ":", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "/:", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "acme:", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": ":plan", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "/acme:", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "a::b", + "error": "identity.invalid" + }, + { + "identity": "@evil/plan", + "alias": "x/$ak-plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@agentkit/plan", + "alias": "x/$ak-plan", + "error": null + }, + { + "identity": "@acme/plan", + "alias": "$", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "$$", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "/$:x", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "a/-/b", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "/a:b:c", + "error": null + }, + { + "identity": "@acme/plan", + "alias": "/a:$:c", + "error": "identity.invalid" + } + ], + "directoryAliases": [ + { + "identity": "@agentkit/plan", + "alias": "ak-plan", + "error": null, + "groupKey": "ak-plan" + }, + { + "identity": "@acme/foo", + "alias": "Foo", + "error": null, + "groupKey": "foo" + }, + { + "identity": "@other/foo", + "alias": "foo", + "error": null, + "groupKey": "foo" + }, + { + "identity": "@acme/foo", + "alias": "foo-2", + "error": null, + "groupKey": "foo-2" + }, + { + "identity": "@acme/plan", + "alias": "con", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "CON", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "aux.md", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "plan.", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "plan ", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "a:b", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "a$b", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "a/b", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "PROGRA~1", + "error": "identity.invalid" + }, + { + "identity": "@evil/plan", + "alias": "ak-plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@evil/plan", + "alias": "AK-Plan", + "error": "identity.reserved_alias" + }, + { + "identity": "@acme/plan", + "alias": ".ak-tmp.plan", + "error": "identity.invalid" + }, + { + "identity": "@acme/plan", + "alias": "---", + "error": "identity.invalid" + } + ], + "directoryAliasCollisions": [ + { + "aliases": ["Foo", "foo"], + "collide": true + }, + { + "aliases": ["foo", "foo-2"], + "collide": false + } + ], + "exposure": [ + { + "frontmatter": "", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "user-invocable: false", + "exposure": "INTERNAL", + "warning": false + }, + { + "frontmatter": "user-invocable: False", + "exposure": "INTERNAL", + "warning": false + }, + { + "frontmatter": "user-invocable: FALSE", + "exposure": "INTERNAL", + "warning": false + }, + { + "frontmatter": "user-invocable: fAlse", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "user-invocable: \"false\"", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "user-invocable: no", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "user-invocable: off", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: true", + "exposure": "EXPLICIT", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: True", + "exposure": "EXPLICIT", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: TRUE", + "exposure": "EXPLICIT", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: yes", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: on", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: 'true'", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: 1", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: true\nuser-invocable: false", + "exposure": "INTERNAL", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: true\nuser-invocable: true", + "exposure": "EXPLICIT", + "warning": false + }, + { + "frontmatter": "disable-model-invocation: true\nuser-invocable: yes", + "exposure": "EXPLICIT", + "warning": false + }, + { + "frontmatter": "user-invocable: false ", + "exposure": "INTERNAL", + "warning": false + }, + { + "frontmatter": "user-invocable: !!bool false", + "exposure": "AUTO", + "warning": true + }, + { + "frontmatter": "user-invocable: &x false", + "exposure": "AUTO", + "warning": true + }, + { + "frontmatter": "a: &x false\nuser-invocable: *x", + "exposure": "AUTO", + "warning": true + }, + { + "frontmatter": "user-invocable: false\nuser-invocable: false", + "exposure": "AUTO", + "warning": true + }, + { + "frontmatter": "user-invocable: [false", + "exposure": "AUTO", + "warning": true + }, + { + "frontmatter": "nested:\n user-invocable: false", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "user-invocable", + "exposure": "AUTO", + "warning": true + }, + { + "frontmatter": "~", + "exposure": "AUTO", + "warning": true + }, + { + "frontmatter": "null", + "exposure": "AUTO", + "warning": true + }, + { + "frontmatter": "- user-invocable: false", + "exposure": "AUTO", + "warning": true + }, + { + "frontmatter": "# only a comment", + "exposure": "AUTO", + "warning": false + }, + { + "frontmatter": "name: a\nname: b\nuser-invocable: false", + "exposure": "INTERNAL", + "warning": false + }, + { + "file": "---\r\nuser-invocable: false\r\n---\r\n# Skill\r\n", + "exposure": "INTERNAL", + "warning": false + }, + { + "file": "--- \ndisable-model-invocation: true\n---\n# Skill\n", + "exposure": "EXPLICIT", + "warning": false + }, + { + "file": "# Skill\n---\nuser-invocable: false\n---\n", + "exposure": "AUTO", + "warning": false + }, + { + "file": "---\nuser-invocable: false\n# no closing line\n", + "exposure": "AUTO", + "warning": true + } + ] +} diff --git a/packages/skill-tree/fixtures/vectors/skill-tree-v1-vectors.json b/packages/skill-tree/fixtures/vectors/skill-tree-v1-vectors.json new file mode 100644 index 0000000..ecb6722 --- /dev/null +++ b/packages/skill-tree/fixtures/vectors/skill-tree-v1-vectors.json @@ -0,0 +1,3091 @@ +{ + "schema": "agentkit.skill-tree-vectors/v1", + "contract": "agentkit.skill-tree/v1", + "spec": "docs/specs/skill-lifecycle-contract.md#4-canonical-tree-agentkitskill-treev1", + "unicode": "15.0.0", + "notes": [ + "files[].content is UTF-8 text; files[].contentBase64 is raw bytes.", + "synthesize[] appends count files: {n} in pathPattern is the zero-based index as five decimal digits, and each file holds size bytes of value fill, with mode 100644 unless mode is given.", + "files[] come before synthesized files; input order never changes a digest.", + "files[].type is the entry kind an archive reader or walker reports: file (the default), fs-hardlink and reparse-other are read as regular files; symlink, archive-hardlink, reparse-name-surrogate, reparse-af-unix, reparse-wsl, device, fifo, socket and cont (tar type '7') fail with tree.unsupported_entry; directory and pax-global (a PAX 'g' header) add nothing to the tree but count toward the entry limit. A directory entry name must pass the path and segment rules but gets no NFC check; it may end with one '/', which is not part of its path, and '//' is unsafe. A pax-global name is metadata and is not checked.", + "An entry is a header that Go archive/tar Reader.Next returns. PAX 'x' and GNU long-name headers fold into the following entry and never appear in files[].", + "A directory, symlink or reparse-name-surrogate entry whose own name is a noise directory name is noise and is dropped without being followed. A non-directory entry of any type whose own name is a noise file name is noise; a directory with such a name is an ordinary directory.", + "A directory entry also takes part in the file-versus-directory check: a file equal to a directory entry path is tree.unsafe_path, and one equal under case folding is tree.case_collision. Directory entries are compared only with files, never with each other or with implied directories. A directory that a walker presents is a directory entry too, given without a trailing '/'; so an empty walked directory 'a' next to a file 'A' is tree.case_collision.", + "A pax-global entry whose paxRecords include path, linkpath, size or any GNU.sparse.* key fails with tree.unsupported_entry. Global records never apply to other entries.", + "files[].headerMode, when present, is the raw tar header mode in octal and replaces mode: the type bits (0o170000) are removed and the rest must be exactly 0644 or 0755.", + "type tar-nul is an old-style tar type NUL entry: a regular file, or a directory when its name ends with '/'. No other type is reclassified by a trailing '/'; a file entry 'refs/' is tree.unsafe_path.", + "files[].rawPathBase64, when present, replaces path with raw name bytes. Names that are not valid UTF-8 (including an unpaired UTF-16 surrogate, given as its WTF-8 bytes) fail with tree.unsafe_path; zip names are judged the same way whatever their UTF-8 flag says.", + "synthesize[].type, when present, sets the entry type of every synthesized entry.", + "A segment that starts with .ak-tmp. fails with tree.unsafe_path. Segment lengths count UTF-8 bytes, and paths sort by UTF-8 bytes, never by UTF-16 code units.", + "tree.read_failed has no vector: it depends on a read error, not on entry data.", + "files[].paxRecords, when present, are the PAX records of that archive entry.", + "Vectors model archive reading: the total entry limit counts every entry in files[] and synthesize[] of any type, including directory, pax-global, rejected types and noise; the tree root is not an entry; the file limit counts regular-file entries only.", + "files[] and synthesize[] are the entries the input presents. An archive presents every entry, noise included; a directory walker presents noise files it visits but prunes noise directories without presenting their contents.", + "Case collisions compare NFD(fold(NFD(path))) with full case folding (CaseFolding.txt statuses C and F), for files and for every directory prefix.", + "Vectors with expect.error must fail with exactly that code; no digest is defined.", + "Walker conformance materializes each vector as a directory: every non-directory entry becomes a file at its path with its bytes (or the item its type names), every directory entry and every parent of a path becomes a directory, and the walker walks the result, never listing a pruned noise directory or a directory whose own path is unsafe. Each directory it walks counts as one entry. A vector whose entries the host cannot create (devices, case-colliding or reserved names, invalid names on Windows) is skipped on that host.", + "files[].mode other than 100644 and 100755 is a tar header or git index mode. A walker maps permission bits instead and never rejects them: any execute bit gives 100755, otherwise 100644. On a host without modes (Windows), walker runs skip positive vectors that contain 100755.", + "archiveOnly: true marks a vector that binds archive readers only, and walker conformance skips it: one with more than one defect (more than one validation step fails on its own), one with no on-disk form (an archive-only type or field, an empty, '.' or '..' path segment, a NUL, a segment over 255 UTF-16 code units, a file mode other than 100644, 100755 or 120000, two entries with one path, or a file whose path is also a directory), one that tests archive entry counting (limit-entries-count-noise), or one whose materialized walk gives a different result, such as an entry count that the walked parent directories change.", + "A positive vector without expect.serialization defines only the digest." + ], + "vectors": [ + { + "id": "skill-md-only", + "description": "SKILL.md only", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "---\nname: ak-demo\ndescription: Demo skill.\n---\n\n# Demo\n" + } + ], + "expect": { + "digest": "sha256:9bc8434a1aed589954b02ca5456c5fe6caa90eda6bcfec8fba3fd0c041e31072", + "serialization": "agentkit.skill-tree/v1\n100644 ec6d8a678a93b3b18a897464bb6822196ce268d5bf60496fc8a58d8efc9ae975 55 SKILL.md\n" + } + }, + { + "id": "empty-file", + "description": "a zero-byte file is a valid entry", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "# x\n" + }, + { + "path": "assets/empty.txt", + "mode": "100644", + "content": "" + } + ], + "expect": { + "digest": "sha256:31151fd31caaceb55e3eda419340a67a5e77dbad94985c5336b8d86c3b44f8aa", + "serialization": "agentkit.skill-tree/v1\n100644 253b610bd786f2543252c8d4b45bb40eb375882587f825cb9ce1fd1241f66f1e 4 SKILL.md\n100644 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 assets/empty.txt\n" + } + }, + { + "id": "nested-resources", + "description": "references, scripts, assets and modules", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "---\nname: ak-nested\ndescription: Nested.\n---\n" + }, + { + "path": "references/guide.md", + "mode": "100644", + "content": "# Guide\n" + }, + { + "path": "references/deep/a/b/c.md", + "mode": "100644", + "content": "deep\n" + }, + { + "path": "scripts/run.sh", + "mode": "100755", + "content": "#!/bin/sh\necho ok\n" + }, + { + "path": "scripts/lib/util.py", + "mode": "100644", + "content": "print('ok')\n" + }, + { + "path": "assets/logo.bin", + "mode": "100644", + "contentBase64": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==" + } + ], + "expect": { + "digest": "sha256:9b7717f1b8c9906a84ca6ac4a3f4464aae53b6aed8973aaa5f9321e69b426b30", + "serialization": "agentkit.skill-tree/v1\n100644 c1640b40572bdcc4ea152cd5a5199c53be4e3edf45b4b459f87faca0ab6fb9aa 45 SKILL.md\n100644 40aff2e9d2d8922e47afd4648e6967497158785fbd1da870e7110266bf944880 256 assets/logo.bin\n100644 64896f89fd11190013b70103e603a1c5826e56b7fb7d2197ab279b0690043599 5 references/deep/a/b/c.md\n100644 bc553ffe57e544498b12a9865dbf3abc2004c474e349c52c378eaa402287424b 8 references/guide.md\n100644 ad64355106bb158b020ecf9702be48f7730fc091dd4bb6a2f092b40393495b3d 12 scripts/lib/util.py\n100755 b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c 18 scripts/run.sh\n" + } + }, + { + "id": "input-order-independent", + "description": "nested-resources listed in reverse order", + "files": [ + { + "path": "assets/logo.bin", + "mode": "100644", + "contentBase64": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==" + }, + { + "path": "scripts/lib/util.py", + "mode": "100644", + "content": "print('ok')\n" + }, + { + "path": "scripts/run.sh", + "mode": "100755", + "content": "#!/bin/sh\necho ok\n" + }, + { + "path": "references/deep/a/b/c.md", + "mode": "100644", + "content": "deep\n" + }, + { + "path": "references/guide.md", + "mode": "100644", + "content": "# Guide\n" + }, + { + "path": "SKILL.md", + "mode": "100644", + "content": "---\nname: ak-nested\ndescription: Nested.\n---\n" + } + ], + "expect": { + "digest": "sha256:9b7717f1b8c9906a84ca6ac4a3f4464aae53b6aed8973aaa5f9321e69b426b30" + } + }, + { + "id": "mode-differs", + "description": "same bytes as nested-resources but run.sh is not executable", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "---\nname: ak-nested\ndescription: Nested.\n---\n" + }, + { + "path": "references/guide.md", + "mode": "100644", + "content": "# Guide\n" + }, + { + "path": "references/deep/a/b/c.md", + "mode": "100644", + "content": "deep\n" + }, + { + "path": "scripts/run.sh", + "mode": "100644", + "content": "#!/bin/sh\necho ok\n" + }, + { + "path": "scripts/lib/util.py", + "mode": "100644", + "content": "print('ok')\n" + }, + { + "path": "assets/logo.bin", + "mode": "100644", + "contentBase64": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==" + } + ], + "expect": { + "digest": "sha256:fc8f3e327b2f3fb536150dd78173fb53e2da8add0b60921bc55754573f979174", + "serialization": "agentkit.skill-tree/v1\n100644 c1640b40572bdcc4ea152cd5a5199c53be4e3edf45b4b459f87faca0ab6fb9aa 45 SKILL.md\n100644 40aff2e9d2d8922e47afd4648e6967497158785fbd1da870e7110266bf944880 256 assets/logo.bin\n100644 64896f89fd11190013b70103e603a1c5826e56b7fb7d2197ab279b0690043599 5 references/deep/a/b/c.md\n100644 bc553ffe57e544498b12a9865dbf3abc2004c474e349c52c378eaa402287424b 8 references/guide.md\n100644 ad64355106bb158b020ecf9702be48f7730fc091dd4bb6a2f092b40393495b3d 12 scripts/lib/util.py\n100644 b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c 18 scripts/run.sh\n" + } + }, + { + "id": "byte-order-sort", + "description": "sort is by UTF-8 bytes: uppercase before lowercase, '-' before '/'", + "files": [ + { + "path": "b.md", + "mode": "100644", + "content": "b\n" + }, + { + "path": "B2.md", + "mode": "100644", + "content": "B\n" + }, + { + "path": "a-b.md", + "mode": "100644", + "content": "x\n" + }, + { + "path": "a/b.md", + "mode": "100644", + "content": "y\n" + }, + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + } + ], + "expect": { + "digest": "sha256:223c0846c918d769aba4428a1e67fed9bd6533115d451f4148d5185e44b410d5", + "serialization": "agentkit.skill-tree/v1\n100644 c0cde77fa8fef97d476c10aad3d2d54fcc2f336140d073651c2dcccf1e379fd6 2 B2.md\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac 2 a-b.md\n100644 3bb2abb69ebb27fbfe63c7639624c6ec5e331b841a5bc8c3ebc10b9285e90877 2 a/b.md\n100644 0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f 2 b.md\n" + } + }, + { + "id": "unicode-nfc", + "description": "an NFC non-ASCII path", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "references/café.md", + "mode": "100644", + "content": "café\n" + } + ], + "expect": { + "digest": "sha256:dc15e15e754f7fb2f6424b22ede37f80bdcc62db26fc6e712557111eee9fe521", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 7b49b9e063bd91a4f9252b413261f5557b9c570aa61516989499f64a62dbcdd6 6 references/café.md\n" + } + }, + { + "id": "crlf-preserved", + "description": "line endings are bytes; no normalization", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "line1\r\nline2\r\n" + } + ], + "expect": { + "digest": "sha256:846cc6452343bb974f16a18c8150f5c3018488d21f7d24a4789296368cfdcf48", + "serialization": "agentkit.skill-tree/v1\n100644 4ad3ef64dfb83f7a8f789bce6f30cc1f8d18491b14db4c875309b150d2a7d213 14 SKILL.md\n" + } + }, + { + "id": "tilde-not-short-name", + "description": "a tilde not followed by a digit, or in a base longer than 8 bytes, is allowed", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "my~notes.md", + "mode": "100644", + "content": "x\n" + }, + { + "path": "abcdefghi~1.md", + "mode": "100644", + "content": "y\n" + } + ], + "expect": { + "digest": "sha256:ec5cda9e95f4d5eb8d705d0a889adc38a222be6759654060ece96c367c93972e", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 3bb2abb69ebb27fbfe63c7639624c6ec5e331b841a5bc8c3ebc10b9285e90877 2 abcdefghi~1.md\n100644 73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac 2 my~notes.md\n" + } + }, + { + "id": "dotted-capital-i-vs-i", + "description": "U+0130 fully folds to 'i' plus U+0307, which stays distinct from 'i'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "İ.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "i.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "digest": "sha256:9f13a5f0b5de100ad63e7e5fe02caf0063f5159492fb929e84eb67c29f7fd33a", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f 2 i.md\n100644 87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7 2 İ.md\n" + } + }, + { + "id": "dotless-i-vs-i", + "description": "U+0131 has no C or F case folding, so it stays distinct from 'i'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "ı.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "i.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "digest": "sha256:bc563cbbc73d784a65958e391e10e1aea647eb74be01e5e821452935b4c240cd", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f 2 i.md\n100644 87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7 2 ı.md\n" + } + }, + { + "id": "noise-excluded", + "description": "noise entries are dropped before hashing; digest equals skill-md-only", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "---\nname: ak-demo\ndescription: Demo skill.\n---\n\n# Demo\n" + }, + { + "path": ".DS_Store", + "mode": "100644", + "content": "junk" + }, + { + "path": "scripts/__pycache__/x.pyc", + "mode": "100644", + "content": "junk" + }, + { + "path": ".git/HEAD", + "mode": "100644", + "content": "ref\n" + }, + { + "path": "references/Thumbs.db", + "mode": "100644", + "content": "junk" + } + ], + "expect": { + "digest": "sha256:9bc8434a1aed589954b02ca5456c5fe6caa90eda6bcfec8fba3fd0c041e31072" + } + }, + { + "id": "noise-dropped-before-validation", + "description": "entries under noise directories are dropped before type and path checks", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "---\nname: ak-demo\ndescription: Demo skill.\n---\n\n# Demo\n" + }, + { + "path": ".git/CON", + "mode": "100644", + "content": "x\n" + }, + { + "path": "__pycache__/link", + "mode": "120000", + "content": "../x" + } + ], + "expect": { + "digest": "sha256:9bc8434a1aed589954b02ca5456c5fe6caa90eda6bcfec8fba3fd0c041e31072" + } + }, + { + "id": "noise-exact-case", + "description": "noise names match exactly; .GIT/ and thumbs.db are content", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": ".GIT/config", + "mode": "100644", + "content": "c\n" + }, + { + "path": "thumbs.db", + "mode": "100644", + "content": "t\n" + } + ], + "expect": { + "digest": "sha256:7046b4fa72a5a9af6d3327b84f3fe9db4f967757f438b02d3c8a5baf538f0b8d", + "serialization": "agentkit.skill-tree/v1\n100644 a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478 2 .GIT/config\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 fe8edeeb98cc6d3b93cf2d57000254b84bd9eba34b4df7ce4b87db8b937b7703 2 thumbs.db\n" + } + }, + { + "id": "noise-junction-dropped", + "description": "a junction named .git is dropped as noise and never followed", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "---\nname: ak-demo\ndescription: Demo skill.\n---\n\n# Demo\n" + }, + { + "path": ".git", + "mode": "100644", + "type": "reparse-name-surrogate", + "content": "" + } + ], + "expect": { + "digest": "sha256:9bc8434a1aed589954b02ca5456c5fe6caa90eda6bcfec8fba3fd0c041e31072" + } + }, + { + "id": "noise-symlink-dropped", + "description": "a symbolic link named __pycache__ is dropped as noise and never followed", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "---\nname: ak-demo\ndescription: Demo skill.\n---\n\n# Demo\n" + }, + { + "path": "scripts/__pycache__", + "mode": "120000", + "type": "symlink", + "content": "/tmp" + } + ], + "expect": { + "digest": "sha256:9bc8434a1aed589954b02ca5456c5fe6caa90eda6bcfec8fba3fd0c041e31072" + } + }, + { + "id": "git-file-is-content", + "description": "a file named .git (not a directory) is content", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": ".git", + "mode": "100644", + "content": "gitdir: ../x\n" + } + ], + "expect": { + "digest": "sha256:5900ff0fa2963ebcca7b38d25db68df667a5fb604e5f82adc1b2e64887b8ac88", + "serialization": "agentkit.skill-tree/v1\n100644 fed6d21816e6fb4c12b8d6c46fec90a93fba25d2b0da2fe0292314bd04adbdba 13 .git\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n" + } + }, + { + "id": "abs-path", + "description": "rejected path \"/etc/passwd\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "/etc/passwd", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "dotdot", + "description": "rejected path \"references/../../x.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "references/../../x.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "dot-segment", + "description": "rejected path \"./SKILL.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "./SKILL.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "empty-segment", + "description": "rejected path \"references//a.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "references//a.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "backslash", + "description": "rejected path \"references\\\\a.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "references\\a.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "colon-drive", + "description": "rejected path \"C:/x.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "C:/x.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "colon-ads", + "description": "rejected path \"SKILL.md:evil\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "SKILL.md:evil", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "lt", + "description": "rejected path \"ab.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a>b.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "dquote", + "description": "rejected path \"a\\\"b.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a\"b.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "pipe", + "description": "rejected path \"a|b.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a|b.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "question", + "description": "rejected path \"a?.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a?.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "star", + "description": "rejected path \"a*.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a*.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "nul", + "description": "rejected path \"a\\u0000b.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a\u0000b.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "newline", + "description": "rejected path \"a\\nb.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a\nb.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "del", + "description": "rejected path \"a\\u007fb.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "ab.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "c1-control", + "description": "rejected path \"a\\u0085b.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a…b.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "cf-zero-width-space", + "description": "rejected path \"SKILL\\u200b.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "SKILL​.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "cf-rtl-override", + "description": "rejected path \"a\\u202eb.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a‮b.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "cf-bom", + "description": "rejected path \"\\ufeffa.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "unassigned", + "description": "rejected path \"\\u0378.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "͸.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "unassigned-in-15-0", + "description": "rejected path \"\\u2ffc.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "⿼.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "trailing-dot", + "description": "rejected path \"notes.\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "notes.", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "trailing-space", + "description": "rejected path \"notes \"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "notes ", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "reserved-con", + "description": "rejected path \"CON\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "CON", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "reserved-lpt1-ext", + "description": "rejected path \"assets/lpt1.txt\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "assets/lpt1.txt", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "reserved-nul-lower", + "description": "rejected path \"nul.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "nul.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "reserved-first-dot", + "description": "rejected path \"nul.tar.gz\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "nul.tar.gz", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "reserved-space-before-ext", + "description": "rejected path \"CON .txt\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "CON .txt", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "reserved-com0", + "description": "rejected path \"com0.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "com0.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "reserved-com-superscript", + "description": "rejected path \"COM\\u00b9.txt\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "COM¹.txt", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "reserved-conin", + "description": "rejected path \"CONIN$\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "CONIN$", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "reserved-conout-ext", + "description": "rejected path \"conout$.log\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "conout$.log", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "short-name", + "description": "rejected path \"PROGRA~1\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "PROGRA~1", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "short-name-ext", + "description": "rejected path \"longfi~1.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "longfi~1.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "short-name-dir", + "description": "rejected path \"docs~2/a.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "docs~2/a.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "segment-too-long", + "description": "rejected path \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "path-too-long", + "description": "rejected path \"dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/f.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/dddddddddddddddddddddddddddddddddddddddddddddddddd/f.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "too-deep", + "description": "rejected path \"d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/f.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/f.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "non-nfc", + "description": "rejected path \"references/cafe\\u0301.md\"", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "references/café.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.non_nfc_path" + } + }, + { + "id": "case-collision", + "description": "SKILL.md vs skill.md", + "files": [ + { + "path": "x.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "SKILL.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "skill.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-latin1", + "description": "U+00C9 vs U+00E9", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "É.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "é.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-capital-sharp-s", + "description": "U+1E9E folds to U+00DF", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "ẞ.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "ß.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-long-s", + "description": "U+017F folds to 's'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "ſ.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "s.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-final-sigma", + "description": "U+03C2 folds to U+03C3", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "ς.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "σ.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-capital-sigma", + "description": "U+03A3 and U+03C2 meet at U+03C3", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "Σ.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "ς.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-cherokee", + "description": "U+AB70 folds to U+13A0 under CaseFolding.txt", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "Ꭰ.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "ꭰ.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-cherokee-small", + "description": "U+13F8 folds to U+13F0 under CaseFolding.txt", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "Ᏸ.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "ᏸ.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-sharp-s-ss", + "description": "U+00DF fully folds to 'ss'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "ß.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "ss.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-capital-sharp-s-ss", + "description": "U+1E9E fully folds to 'ss'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "ẞ.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "ss.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-fi-ligature", + "description": "U+FB01 is NFC and fully folds to 'fi'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "fi.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "fi.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "case-collision-decomposed-fold", + "description": "U+1E9B folds to U+1E61; both sides meet as 's' plus U+0307 after NFD", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "ẛ.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "ṡ.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "kelvin-sign-not-nfc", + "description": "KELVIN SIGN U+212A is not NFC (it decomposes to 'K'), so it fails before case folding", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "K.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.non_nfc_path" + } + }, + { + "id": "file-dir-conflict", + "description": "a file 'a' and a path under 'a/' with the same name", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a", + "mode": "100644", + "content": "x\n" + }, + { + "path": "a/b.md", + "mode": "100644", + "content": "y\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "file-dir-conflict-case", + "description": "a file 'A' and a path under 'a/' that differ only by case", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "A", + "mode": "100644", + "content": "x\n" + }, + { + "path": "a/b.md", + "mode": "100644", + "content": "y\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "dir-case-collision", + "description": "directories 'A' and 'a' differ only by case and would merge", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "A/x.md", + "mode": "100644", + "content": "x\n" + }, + { + "path": "a/y.md", + "mode": "100644", + "content": "y\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "nested-dir-case-collision", + "description": "nested directories that differ only by case", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "refs/Docs/x.md", + "mode": "100644", + "content": "x\n" + }, + { + "path": "refs/docs/y.md", + "mode": "100644", + "content": "y\n" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "reserved-name-ascii-only-dotless-i", + "description": "reserved names map only ASCII a-z to A-Z, so U+0131 never matches 'I'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "conın$.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "digest": "sha256:7dceaa1e6864d00e7b653f83e091237775b51fcec93e6be7d194433a4adc5831", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac 2 conın$.md\n" + } + }, + { + "id": "reserved-name-ascii-only-dotted-i", + "description": "reserved names map only ASCII a-z to A-Z, so U+0130 never matches 'I'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "conİn$.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "digest": "sha256:eca107117cd188102d1fb336077747b777487dea0f32d01b6319fed5b1ecae69", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac 2 conİn$.md\n" + } + }, + { + "id": "depth-at-max", + "description": "exactly 32 segments, counting the file name", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/f.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "digest": "sha256:d518869768d056d8f1fb20024424c6e44006e2d50fb81a90873f384b926ffa03", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac 2 d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/d/f.md\n" + } + }, + { + "id": "directory-entry-adds-nothing", + "description": "an archive directory entry and a PAX global header add nothing to the tree", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "references/", + "mode": "100644", + "type": "directory", + "content": "" + }, + { + "path": "pax_global_header", + "mode": "100644", + "type": "pax-global", + "content": "" + } + ], + "expect": { + "digest": "sha256:96732f9ead4e00966bdd17e6157641ebdc9d2208d8f7aadb01fa18dbf089e53d", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n" + }, + "archiveOnly": true + }, + { + "id": "directory-entry-unsafe-name", + "description": "an archive directory entry name must pass the path rules", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "../x/", + "mode": "100644", + "type": "directory", + "content": "" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "directory-entry-name-path-rules-only", + "description": "directory entry names get only the path rules: a non-NFC name and a case-only twin of a file's directory are accepted and add nothing", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "refs/x.md", + "mode": "100644", + "content": "x\n" + }, + { + "path": "Refs/", + "mode": "100644", + "type": "directory", + "content": "" + }, + { + "path": "café/", + "mode": "100644", + "type": "directory", + "content": "" + } + ], + "expect": { + "digest": "sha256:6106f63ecba8ca6cde8b1ba75f62603d371872af384bf6dd9955504f12d9b6df", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac 2 refs/x.md\n" + } + }, + { + "id": "pax-global-name-not-checked", + "description": "PAX global header names are metadata and are not checked, so the names Python tarfile and GNU tar write are accepted", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "../pax_global_header", + "mode": "100644", + "type": "pax-global", + "content": "" + }, + { + "path": "././@PaxHeader", + "mode": "100644", + "type": "pax-global", + "content": "" + }, + { + "path": "/tmp/GlobalHead.4242.1", + "mode": "100644", + "type": "pax-global", + "content": "" + } + ], + "expect": { + "digest": "sha256:96732f9ead4e00966bdd17e6157641ebdc9d2208d8f7aadb01fa18dbf089e53d", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n" + }, + "archiveOnly": true + }, + { + "id": "pax-global-unchecked-name-records-rejected", + "description": "a PAX global header with an unchecked name still fails on a path record", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "././@PaxHeader", + "mode": "100644", + "type": "pax-global", + "content": "", + "paxRecords": { + "path": "x.md" + } + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "pax-global-record-path", + "description": "a PAX global header carrying a path record is not supported", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "pax_global_header", + "mode": "100644", + "type": "pax-global", + "content": "", + "paxRecords": { + "path": "x.md" + } + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "pax-global-record-linkpath", + "description": "a PAX global header carrying a linkpath record is not supported", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "pax_global_header", + "mode": "100644", + "type": "pax-global", + "content": "", + "paxRecords": { + "linkpath": "x.md" + } + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "pax-global-record-size", + "description": "a PAX global header carrying a size record is not supported", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "pax_global_header", + "mode": "100644", + "type": "pax-global", + "content": "", + "paxRecords": { + "size": "1" + } + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "pax-global-record-gnu-sparse-major", + "description": "a PAX global header carrying a GNU.sparse.major record is not supported", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "pax_global_header", + "mode": "100644", + "type": "pax-global", + "content": "", + "paxRecords": { + "GNU.sparse.major": "1" + } + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "pax-global-comment-ignored", + "description": "a PAX global header with only a comment record, as git archive writes, is accepted and applies to no entry", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "pax_global_header", + "mode": "100644", + "type": "pax-global", + "content": "", + "paxRecords": { + "comment": "0123456789abcdef0123456789abcdef01234567" + } + } + ], + "expect": { + "digest": "sha256:96732f9ead4e00966bdd17e6157641ebdc9d2208d8f7aadb01fa18dbf089e53d", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n" + }, + "archiveOnly": true + }, + { + "id": "header-mode-setuid", + "description": "a tar header mode 04755 keeps the setuid bit after the type bits are removed", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "run.sh", + "headerMode": "4755", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "header-mode-type-bits", + "description": "a tar header mode 0100644 is 0644 after the type bits are removed", + "files": [ + { + "path": "SKILL.md", + "headerMode": "100644", + "content": "s\n" + } + ], + "expect": { + "digest": "sha256:96732f9ead4e00966bdd17e6157641ebdc9d2208d8f7aadb01fa18dbf089e53d", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n" + }, + "archiveOnly": true + }, + { + "id": "tar-type-nul", + "description": "an old-style tar type NUL entry is a regular file, or a directory when its name ends with '/'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "notes.md", + "mode": "100644", + "type": "tar-nul", + "content": "n\n" + }, + { + "path": "refs/", + "mode": "100644", + "type": "tar-nul", + "content": "" + } + ], + "expect": { + "digest": "sha256:097a4ad327d9a2586a6b7c80dd99d4a6c20f96ac5e23d0202d7a7d621a137f6e" + }, + "archiveOnly": true + }, + { + "id": "noise-file-name-special-dropped", + "description": "a symbolic link or special file with a noise file name is dropped as noise", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "---\nname: ak-demo\ndescription: Demo skill.\n---\n\n# Demo\n" + }, + { + "path": ".DS_Store", + "mode": "120000", + "type": "symlink", + "content": "/tmp" + }, + { + "path": "references/Thumbs.db", + "mode": "100644", + "type": "fifo", + "content": "" + } + ], + "expect": { + "digest": "sha256:9bc8434a1aed589954b02ca5456c5fe6caa90eda6bcfec8fba3fd0c041e31072" + } + }, + { + "id": "file-vs-directory-entry", + "description": "a file 'a' and a directory entry 'a/' name the same path", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a", + "mode": "100644", + "content": "x\n" + }, + { + "path": "a/", + "mode": "100644", + "type": "directory", + "content": "" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "file-vs-directory-entry-case", + "description": "a file 'a' and a directory entry 'A/' differ only by case", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a", + "mode": "100644", + "content": "x\n" + }, + { + "path": "A/", + "mode": "100644", + "type": "directory", + "content": "" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "raw-name-invalid-utf8", + "description": "a raw name byte sequence that is not UTF-8 is never replaced by U+FFFD", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "rawPathBase64": "YmFk/y5tZA==", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "raw-name-unpaired-surrogate", + "description": "an unpaired UTF-16 surrogate in a name, given as its WTF-8 bytes", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "rawPathBase64": "7aCALm1k", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "contiguous-file-entry", + "description": "a tar contiguous-file entry (type '7') is not supported", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "data.bin", + "mode": "100644", + "type": "cont", + "content": "x" + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "mode-0664", + "description": "permissions must be exactly 0644 or 0755", + "files": [ + { + "path": "SKILL.md", + "mode": "100664", + "content": "s\n" + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "gitlink-160000", + "description": "a git index gitlink (mode 160000) is not a regular file", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "vendor", + "mode": "160000", + "content": "" + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "dir-prefix-not-conflict", + "description": "'a' as a directory and 'ab' as a file do not conflict", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a/b.md", + "mode": "100644", + "content": "x\n" + }, + { + "path": "ab", + "mode": "100644", + "content": "y\n" + } + ], + "expect": { + "digest": "sha256:5ffb8a63647dad3ce84f92291000412ff0702080d376a6255381387622e58ae9", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac 2 a/b.md\n100644 3bb2abb69ebb27fbfe63c7639624c6ec5e331b841a5bc8c3ebc10b9285e90877 2 ab\n" + } + }, + { + "id": "archive-hardlink", + "description": "an archive hard-link entry", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "copy.md", + "mode": "100644", + "type": "archive-hardlink", + "content": "" + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "junction", + "description": "a Windows junction, which is a name-surrogate reparse point", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "refs", + "mode": "100644", + "type": "reparse-name-surrogate", + "content": "" + } + ], + "expect": { + "error": "tree.unsupported_entry" + } + }, + { + "id": "af-unix-reparse", + "description": "a Windows AF_UNIX socket reparse point", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "agent.sock", + "mode": "100644", + "type": "reparse-af-unix", + "content": "" + } + ], + "expect": { + "error": "tree.unsupported_entry" + } + }, + { + "id": "wsl-reparse", + "description": "a WSL special-file reparse point", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "fifo", + "mode": "100644", + "type": "reparse-wsl", + "content": "" + } + ], + "expect": { + "error": "tree.unsupported_entry" + } + }, + { + "id": "gnu-sparse-pax", + "description": "a regular-file entry carrying a GNU.sparse PAX record", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "data.bin", + "mode": "100644", + "content": "x", + "paxRecords": { + "GNU.sparse.major": "1", + "GNU.sparse.minor": "0" + } + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "fifo-entry", + "description": "a FIFO", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "pipe", + "mode": "100644", + "type": "fifo", + "content": "" + } + ], + "expect": { + "error": "tree.unsupported_entry" + } + }, + { + "id": "device-entry", + "description": "a device node", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "dev", + "mode": "100644", + "type": "device", + "content": "" + } + ], + "expect": { + "error": "tree.unsupported_entry" + } + }, + { + "id": "socket-entry", + "description": "a socket", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "sock", + "mode": "100644", + "type": "socket", + "content": "" + } + ], + "expect": { + "error": "tree.unsupported_entry" + } + }, + { + "id": "fs-hardlink-read-as-file", + "description": "a filesystem hard link is read as an independent regular file", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "copy.md", + "mode": "100644", + "type": "fs-hardlink", + "content": "s\n" + } + ], + "expect": { + "digest": "sha256:575779d711e2542f300e742f99d46bb09b18e617d797dac0cea3a3aefad1c82e", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 copy.md\n" + } + }, + { + "id": "cloud-placeholder-read-as-file", + "description": "a reparse point without the name-surrogate bit, such as a cloud placeholder, is read as a regular file", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "notes.md", + "mode": "100644", + "type": "reparse-other", + "content": "n\n" + } + ], + "expect": { + "digest": "sha256:097a4ad327d9a2586a6b7c80dd99d4a6c20f96ac5e23d0202d7a7d621a137f6e", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 a4fb621495a0122493b2203591c448903c472e306a1ede54fabad829e01075c0 2 notes.md\n" + } + }, + { + "id": "duplicate-path", + "description": "the same path listed twice", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "SKILL.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "error": "tree.case_collision" + }, + "archiveOnly": true + }, + { + "id": "bad-mode", + "description": "mode must be 100644 or 100755", + "files": [ + { + "path": "SKILL.md", + "mode": "100777", + "content": "s\n" + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "symlink-entry", + "description": "a symlink entry is never followed or stored", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "link", + "mode": "120000", + "content": "../outside" + } + ], + "expect": { + "error": "tree.unsupported_entry" + } + }, + { + "id": "empty-tree", + "description": "a tree must contain at least one file", + "files": [], + "expect": { + "error": "tree.empty" + } + }, + { + "id": "only-noise", + "description": "a tree that is empty after noise removal", + "files": [ + { + "path": ".DS_Store", + "mode": "100644", + "content": "x" + }, + { + "path": ".git/HEAD", + "mode": "100644", + "content": "r\n" + } + ], + "expect": { + "error": "tree.empty" + } + }, + { + "id": "order-type-before-path", + "description": "a bad mode wins over an unsafe path", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "a/../x.md", + "mode": "100777", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsupported_entry" + }, + "archiveOnly": true + }, + { + "id": "order-path-before-nfc", + "description": "an unsafe segment wins over a non-NFC segment", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "café/CON", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "order-nfc-before-collision", + "description": "a non-NFC path wins over a case collision", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "skill.md", + "mode": "100644", + "content": "x\n" + }, + { + "path": "café.md", + "mode": "100644", + "content": "y\n" + } + ], + "expect": { + "error": "tree.non_nfc_path" + }, + "archiveOnly": true + }, + { + "id": "order-path-before-collision", + "description": "an unsafe path wins over a case collision", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "skill.md", + "mode": "100644", + "content": "x\n" + }, + { + "path": "a?.md", + "mode": "100644", + "content": "y\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "utf8-order-not-utf16", + "description": "sort is by UTF-8 bytes: U+FF5E sorts before U+1F600, which UTF-16 code units would order the other way", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "~.md", + "mode": "100644", + "content": "a\n" + }, + { + "path": "😀.md", + "mode": "100644", + "content": "b\n" + } + ], + "expect": { + "digest": "sha256:c0951b75f4d03981fd86ddf53a8b8a17b20639674ae35c90538ce0e44070567c", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7 2 ~.md\n100644 0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f 2 😀.md\n" + } + }, + { + "id": "segment-255-bytes-multibyte", + "description": "a segment of 85 three-byte characters is exactly 255 bytes", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "あああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "digest": "sha256:fa2e5bb3fc7f10f55800420621ea84ceae696d469751577a318ab90998d6bd2f", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac 2 あああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ\n" + } + }, + { + "id": "segment-too-long-multibyte", + "description": "a segment of 86 three-byte characters is 258 bytes", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "ああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "segment-too-long-astral", + "description": "a segment of 64 four-byte characters is 256 bytes, though only 128 UTF-16 units", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "short-name-multibyte-base", + "description": "the 8-byte short-name test counts UTF-8 bytes, so a ten-byte base with '~1' is allowed", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "éééé~1.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "digest": "sha256:6d3cf75ae7ac66f049ae662d16e55da21dcd64de498e73de93cf95e2d1404992", + "serialization": "agentkit.skill-tree/v1\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n100644 73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac 2 éééé~1.md\n" + } + }, + { + "id": "ak-tmp-segment", + "description": "a segment that starts with .ak-tmp. is reserved for AgentKit temporary files", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "references/.ak-tmp.notes.md", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + } + }, + { + "id": "tar-regular-trailing-slash", + "description": "only an old-style type NUL entry becomes a directory by a trailing '/'; a regular-file entry 'refs/' has an empty last segment", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "refs/", + "mode": "100644", + "content": "x\n" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "directory-entry-double-slash", + "description": "a directory entry name has exactly one trailing '/'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "refs//", + "mode": "100644", + "type": "directory", + "content": "" + } + ], + "expect": { + "error": "tree.unsafe_path" + }, + "archiveOnly": true + }, + { + "id": "noise-file-name-directory-kept", + "description": "a directory named like a noise file is an ordinary directory and its contents are kept", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": ".DS_Store/", + "mode": "100644", + "type": "directory", + "content": "" + }, + { + "path": ".DS_Store/notes.md", + "mode": "100644", + "content": "n\n" + } + ], + "expect": { + "digest": "sha256:292407740f5dbaec5d1da463d240ac4a08f2f1750909ff111bc7b5c9f3cc0c34", + "serialization": "agentkit.skill-tree/v1\n100644 a4fb621495a0122493b2203591c448903c472e306a1ede54fabad829e01075c0 2 .DS_Store/notes.md\n100644 cbc80bb5c0c0f8944bf73b3a429505ac5cde16644978bc9a1e74c5755f8ca556 2 SKILL.md\n" + } + }, + { + "id": "walker-empty-directory-case", + "description": "a directory walker presents an empty directory 'a' (no trailing '/'); it collides with a file 'A'", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": "A", + "mode": "100644", + "content": "x\n" + }, + { + "path": "a", + "mode": "100644", + "type": "directory", + "content": "" + } + ], + "expect": { + "error": "tree.case_collision" + } + }, + { + "id": "limit-files-at-max", + "description": "exactly the maximum file count", + "synthesize": [ + { + "pathPattern": "f/{n}.md", + "count": 4096, + "size": 2, + "fill": 120 + } + ], + "expect": { + "digest": "sha256:ae67fe8ab0326491fedc7276186c0c2449be4f4fcb4f5c18b47df01d2428067d" + } + }, + { + "id": "limit-files-over", + "description": "one file over the maximum file count", + "synthesize": [ + { + "pathPattern": "f/{n}.md", + "count": 4097, + "size": 2, + "fill": 120 + } + ], + "expect": { + "error": "tree.limit_exceeded" + } + }, + { + "id": "limit-count-includes-noise", + "description": "a presented noise file counts toward the file limit", + "synthesize": [ + { + "pathPattern": "f/{n}.md", + "count": 4096, + "size": 2, + "fill": 120 + }, + { + "pathPattern": "x/{n}/.DS_Store", + "count": 1, + "size": 1, + "fill": 0 + } + ], + "expect": { + "error": "tree.limit_exceeded" + } + }, + { + "id": "limit-directory-entries-not-counted", + "description": "directory entries do not count toward the file limit", + "synthesize": [ + { + "pathPattern": "f/{n}.md", + "count": 4096, + "size": 2, + "fill": 120 + }, + { + "pathPattern": "d/{n}", + "count": 1, + "size": 0, + "fill": 0, + "type": "directory" + } + ], + "expect": { + "digest": "sha256:ae67fe8ab0326491fedc7276186c0c2449be4f4fcb4f5c18b47df01d2428067d" + } + }, + { + "id": "limit-large-noise-file", + "description": "a presented noise file counts toward the byte limits", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + } + ], + "synthesize": [ + { + "pathPattern": "x/{n}/.DS_Store", + "count": 1, + "size": 33554433, + "fill": 0 + } + ], + "expect": { + "error": "tree.limit_exceeded" + } + }, + { + "id": "limit-entries-at-max", + "description": "exactly the maximum total entry count, directories included", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + } + ], + "synthesize": [ + { + "pathPattern": "d/{n}", + "count": 9999, + "size": 0, + "fill": 0, + "type": "directory" + } + ], + "expect": { + "digest": "sha256:96732f9ead4e00966bdd17e6157641ebdc9d2208d8f7aadb01fa18dbf089e53d" + }, + "archiveOnly": true + }, + { + "id": "limit-entries-over", + "description": "one entry over the maximum total entry count, directories included", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + } + ], + "synthesize": [ + { + "pathPattern": "d/{n}", + "count": 10000, + "size": 0, + "fill": 0, + "type": "directory" + } + ], + "expect": { + "error": "tree.limit_exceeded" + } + }, + { + "id": "limit-entries-count-noise", + "description": "a noise entry counts toward the total entry limit", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + }, + { + "path": ".git/HEAD", + "mode": "100644", + "content": "r\n" + } + ], + "synthesize": [ + { + "pathPattern": "d/{n}", + "count": 9999, + "size": 0, + "fill": 0, + "type": "directory" + } + ], + "expect": { + "error": "tree.limit_exceeded" + }, + "archiveOnly": true + }, + { + "id": "order-limit-first", + "description": "a limit failure wins over a bad mode", + "files": [ + { + "path": "SKILL.md", + "mode": "100777", + "content": "s\n" + } + ], + "synthesize": [ + { + "pathPattern": "f/{n}.md", + "count": 4096, + "size": 2, + "fill": 120 + } + ], + "expect": { + "error": "tree.limit_exceeded" + }, + "archiveOnly": true + }, + { + "id": "limit-file-at-max", + "description": "one file of exactly the maximum file size", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + } + ], + "synthesize": [ + { + "pathPattern": "big/{n}.bin", + "count": 1, + "size": 33554432, + "fill": 0 + } + ], + "expect": { + "digest": "sha256:98b7347cb47e4a9e7a005999abc69318d15d2786cbd0f4c0e1cbe8430bf96256" + } + }, + { + "id": "limit-file-over", + "description": "one file a byte over the maximum file size", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + } + ], + "synthesize": [ + { + "pathPattern": "big/{n}.bin", + "count": 1, + "size": 33554433, + "fill": 0 + } + ], + "expect": { + "error": "tree.limit_exceeded" + } + }, + { + "id": "limit-tree-at-max", + "description": "exactly the maximum tree size", + "synthesize": [ + { + "pathPattern": "big/{n}.bin", + "count": 8, + "size": 33554432, + "fill": 0 + } + ], + "expect": { + "digest": "sha256:939d6faff8840f2b9d1377aafe1b20d1cd235bb072a0ab5d49bc7e8d657664c6" + } + }, + { + "id": "limit-tree-over", + "description": "one byte over the maximum tree size", + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "content": "s\n" + } + ], + "synthesize": [ + { + "pathPattern": "big/{n}.bin", + "count": 8, + "size": 33554432, + "fill": 0 + } + ], + "expect": { + "error": "tree.limit_exceeded" + } + } + ] +} diff --git a/packages/skill-tree/package.json b/packages/skill-tree/package.json new file mode 100644 index 0000000..607a537 --- /dev/null +++ b/packages/skill-tree/package.json @@ -0,0 +1,23 @@ +{ + "name": "@skillx/skill-tree", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "agentkit.skill-tree/v1 canonical tree digest, archive readers, identity and exposure rules shared with AgentKit.", + "exports": { + ".": "./src/index.ts" + }, + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest run", + "gen:unicode": "node scripts/gen-unicode-tables.mjs" + }, + "dependencies": { + "yaml": "^2.9.1" + }, + "devDependencies": { + "@types/node": "^22.19.10", + "typescript": "^5.9.2" + } +} diff --git a/packages/skill-tree/scripts/dump-go-unicode.go b/packages/skill-tree/scripts/dump-go-unicode.go new file mode 100644 index 0000000..9a51c56 --- /dev/null +++ b/packages/skill-tree/scripts/dump-go-unicode.go @@ -0,0 +1,106 @@ +//go:build ignore + +// dump-go-unicode writes the Unicode data the AgentKit reference +// implementation (Go skilltree package) validates paths with: general +// category from the Go unicode tables and canonical normalization from +// golang.org/x/text/unicode/norm, both pinned to Unicode 15.0.0. The output +// is the input of gen-unicode-tables.mjs, so the TypeScript tables match the +// reference exactly. +// +// Run it from the AgentKit apps/cli module, which pins x/text: +// +// cd /apps/cli && go run /packages/skill-tree/scripts/dump-go-unicode.go > /packages/skill-tree/scripts/unicode-15.0.0-go.json +package main + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "unicode" + "unicode/utf8" + + "golang.org/x/text/unicode/norm" +) + +type dump struct { + UnicodeVersion string `json:"unicodeVersion"` + NormVersion string `json:"normVersion"` + GoVersion string `json:"goVersion"` + Assigned [][2]rune `json:"assigned"` + Format [][2]rune `json:"format"` + CombiningClass [][2]rune `json:"combiningClass"` + // Decompositions maps a code point to its canonical decomposition: the + // one-step pair for a primary composite, the full decomposition otherwise. + Decompositions [][]rune `json:"decompositions"` + Composites []rune `json:"composites"` +} + +func addRange(ranges [][2]rune, r rune) [][2]rune { + if n := len(ranges); n > 0 && ranges[n-1][1]+1 == r { + ranges[n-1][1] = r + return ranges + } + return append(ranges, [2]rune{r, r}) +} + +// compositePair finds the canonical pair (a, b) that NFC composes into r: b +// is one code point of the full decomposition and a is the NFC of the rest, +// which must be a single code point. +func compositePair(r rune, full []rune) ([2]rune, bool) { + for k := len(full) - 1; k >= 1; k-- { + rest := append(append([]rune{}, full[:k]...), full[k+1:]...) + a := norm.NFC.String(string(rest)) + if utf8.RuneCountInString(a) != 1 { + continue + } + ar, _ := utf8.DecodeRuneInString(a) + if norm.NFC.String(a+string(full[k])) == string(r) { + return [2]rune{ar, full[k]}, true + } + } + return [2]rune{}, false +} + +func main() { + d := dump{UnicodeVersion: unicode.Version, NormVersion: norm.Version, GoVersion: runtime.Version()} + for r := rune(0); r <= unicode.MaxRune; r++ { + if unicode.In(r, unicode.L, unicode.M, unicode.N, unicode.P, unicode.S, unicode.Z, unicode.Cc, unicode.Cf, unicode.Co, unicode.Cs) { + d.Assigned = addRange(d.Assigned, r) + } + if unicode.Is(unicode.Cf, r) { + d.Format = addRange(d.Format, r) + } + if r >= 0xD800 && r <= 0xDFFF { + continue + } + s := string(r) + props := norm.NFD.PropertiesString(s) + if ccc := props.CCC(); ccc != 0 { + d.CombiningClass = append(d.CombiningClass, [2]rune{r, rune(ccc)}) + } + if r >= 0xAC00 && r <= 0xD7A3 { + continue // Hangul syllables are algorithmic. + } + full := []rune(norm.NFD.String(s)) + if len(full) == 1 && full[0] == r { + continue + } + if norm.NFC.String(s) == s && len(full) > 1 { + pair, ok := compositePair(r, full) + if !ok { + fmt.Fprintf(os.Stderr, "no canonical pair for U+%04X\n", r) + os.Exit(1) + } + d.Decompositions = append(d.Decompositions, []rune{r, pair[0], pair[1]}) + d.Composites = append(d.Composites, r) + continue + } + d.Decompositions = append(d.Decompositions, append([]rune{r}, full...)) + } + enc := json.NewEncoder(os.Stdout) + if err := enc.Encode(d); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/packages/skill-tree/scripts/gen-unicode-tables.mjs b/packages/skill-tree/scripts/gen-unicode-tables.mjs new file mode 100644 index 0000000..16fbe60 --- /dev/null +++ b/packages/skill-tree/scripts/gen-unicode-tables.mjs @@ -0,0 +1,130 @@ +#!/usr/bin/env node +/** + * Generates src/unicode-15-tables.ts, the Unicode 15.0.0 tables of the + * skill-tree path rules (contract 4.3), from two pinned inputs: + * + * - fixtures/ucd/CaseFolding-15.0.0.txt, the Unicode 15.0.0 data file, also + * committed by AgentKit as the source of its fold table; + * - scripts/unicode-15.0.0-go.json, written by dump-go-unicode.go from the Go + * unicode and x/text norm tables (Unicode 15.0.0) that the AgentKit + * reference implementation uses for category and normalization. + * + * node scripts/gen-unicode-tables.mjs + * + * Each input must match its pinned sha256, so the output is reproducible and + * cannot silently move to another Unicode version. + */ +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const VERSION = "15.0.0"; +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const INPUTS = { + caseFolding: { + path: "fixtures/ucd/CaseFolding-15.0.0.txt", + sha256: "cdd49e55eae3bbf1f0a3f6580c974a0263cb86a6a08daa10fbf705b4808a56f7", + }, + goTables: { + path: "scripts/unicode-15.0.0-go.json", + sha256: "baa335155dcbaf0cfe8f40fcc0a98232c4134e821a08c3f198f829a51d911ad5", + }, +}; + +function load({ path, sha256 }) { + const bytes = readFileSync(join(root, path)); + const sum = createHash("sha256").update(bytes).digest("hex"); + if (sum !== sha256) { + throw new Error(`${path}: sha256 ${sum} is not the pinned input ${sha256}`); + } + return { text: bytes.toString("utf8"), sum }; +} + +const b36 = (n) => n.toString(36); + +function encodeRanges(ranges) { + let prev = 0; + return ranges + .map(([lo, hi]) => { + const item = `${b36(lo - prev)}.${b36(hi - lo)}`; + prev = hi; + return item; + }) + .join(","); +} + +function encodeValueRanges(pairs) { + const ranges = []; + for (const [cp, value] of [...pairs].sort((a, b) => a[0] - b[0])) { + const last = ranges[ranges.length - 1]; + if (last && last[1] + 1 === cp && last[2] === value) last[1] = cp; + else ranges.push([cp, cp, value]); + } + let prev = 0; + return ranges + .map(([lo, hi, value]) => { + const item = `${b36(lo - prev)}.${b36(hi - lo)}.${b36(value)}`; + prev = hi; + return item; + }) + .join(","); +} + +function encodeMap(entries) { + let prev = 0; + return [...entries] + .sort((a, b) => a[0] - b[0]) + .map(([cp, values]) => { + const item = `${b36(cp - prev)}:${values.map(b36).join(".")}`; + prev = cp; + return item; + }) + .join(","); +} + +function encodeList(list) { + let prev = 0; + return [...list] + .sort((a, b) => a - b) + .map((cp) => { + const item = b36(cp - prev); + prev = cp; + return item; + }) + .join(","); +} + +const folding = load(INPUTS.caseFolding); +const foldEntries = []; +for (const line of folding.text.split("\n")) { + const fields = line.replace(/#.*/, "").split(";").map((f) => f.trim()); + if (fields.length < 3 || (fields[1] !== "C" && fields[1] !== "F")) continue; + foldEntries.push([parseInt(fields[0], 16), fields[2].split(" ").map((h) => parseInt(h, 16))]); +} + +const go = load(INPUTS.goTables); +const tables = JSON.parse(go.text); +if (tables.unicodeVersion !== VERSION || tables.normVersion !== VERSION) { + throw new Error(`Go tables are Unicode ${tables.unicodeVersion}/${tables.normVersion}, want ${VERSION}`); +} + +const out = `// Code generated by scripts/gen-unicode-tables.mjs. DO NOT EDIT. +// Unicode ${VERSION}. Inputs: +// ${INPUTS.caseFolding.path} sha256 ${folding.sum} +// ${INPUTS.goTables.path} sha256 ${go.sum} (${tables.goVersion}, x/text norm ${tables.normVersion}) +// Encodings: ranges are "delta.length" base-36 items (delta from the previous +// range end); maps are "keyDelta:value.value"; lists are base-36 deltas. + +export const UNICODE_VERSION = "${VERSION}"; +export const ASSIGNED_RANGES = "${encodeRanges(tables.assigned)}"; +export const FORMAT_RANGES = "${encodeRanges(tables.format)}"; +export const COMBINING_CLASSES = "${encodeValueRanges(tables.combiningClass)}"; +export const CANONICAL_DECOMPOSITIONS = "${encodeMap(tables.decompositions.map(([cp, ...m]) => [cp, m]))}"; +export const PRIMARY_COMPOSITES = "${encodeList(tables.composites)}"; +export const CASE_FOLDING = "${encodeMap(foldEntries)}"; +`; + +const target = join(root, "src", "unicode-15-tables.ts"); +writeFileSync(target, out); +console.log(`wrote ${target}`); diff --git a/packages/skill-tree/scripts/unicode-15.0.0-go.json b/packages/skill-tree/scripts/unicode-15.0.0-go.json new file mode 100644 index 0000000..85bb1df --- /dev/null +++ b/packages/skill-tree/scripts/unicode-15.0.0-go.json @@ -0,0 +1 @@ +{"unicodeVersion":"15.0.0","normVersion":"15.0.0","goVersion":"go1.25.3","assigned":[[0,887],[890,895],[900,906],[908,908],[910,929],[931,1327],[1329,1366],[1369,1418],[1421,1423],[1425,1479],[1488,1514],[1519,1524],[1536,1805],[1807,1866],[1869,1969],[1984,2042],[2045,2093],[2096,2110],[2112,2139],[2142,2142],[2144,2154],[2160,2190],[2192,2193],[2200,2435],[2437,2444],[2447,2448],[2451,2472],[2474,2480],[2482,2482],[2486,2489],[2492,2500],[2503,2504],[2507,2510],[2519,2519],[2524,2525],[2527,2531],[2534,2558],[2561,2563],[2565,2570],[2575,2576],[2579,2600],[2602,2608],[2610,2611],[2613,2614],[2616,2617],[2620,2620],[2622,2626],[2631,2632],[2635,2637],[2641,2641],[2649,2652],[2654,2654],[2662,2678],[2689,2691],[2693,2701],[2703,2705],[2707,2728],[2730,2736],[2738,2739],[2741,2745],[2748,2757],[2759,2761],[2763,2765],[2768,2768],[2784,2787],[2790,2801],[2809,2815],[2817,2819],[2821,2828],[2831,2832],[2835,2856],[2858,2864],[2866,2867],[2869,2873],[2876,2884],[2887,2888],[2891,2893],[2901,2903],[2908,2909],[2911,2915],[2918,2935],[2946,2947],[2949,2954],[2958,2960],[2962,2965],[2969,2970],[2972,2972],[2974,2975],[2979,2980],[2984,2986],[2990,3001],[3006,3010],[3014,3016],[3018,3021],[3024,3024],[3031,3031],[3046,3066],[3072,3084],[3086,3088],[3090,3112],[3114,3129],[3132,3140],[3142,3144],[3146,3149],[3157,3158],[3160,3162],[3165,3165],[3168,3171],[3174,3183],[3191,3212],[3214,3216],[3218,3240],[3242,3251],[3253,3257],[3260,3268],[3270,3272],[3274,3277],[3285,3286],[3293,3294],[3296,3299],[3302,3311],[3313,3315],[3328,3340],[3342,3344],[3346,3396],[3398,3400],[3402,3407],[3412,3427],[3430,3455],[3457,3459],[3461,3478],[3482,3505],[3507,3515],[3517,3517],[3520,3526],[3530,3530],[3535,3540],[3542,3542],[3544,3551],[3558,3567],[3570,3572],[3585,3642],[3647,3675],[3713,3714],[3716,3716],[3718,3722],[3724,3747],[3749,3749],[3751,3773],[3776,3780],[3782,3782],[3784,3790],[3792,3801],[3804,3807],[3840,3911],[3913,3948],[3953,3991],[3993,4028],[4030,4044],[4046,4058],[4096,4293],[4295,4295],[4301,4301],[4304,4680],[4682,4685],[4688,4694],[4696,4696],[4698,4701],[4704,4744],[4746,4749],[4752,4784],[4786,4789],[4792,4798],[4800,4800],[4802,4805],[4808,4822],[4824,4880],[4882,4885],[4888,4954],[4957,4988],[4992,5017],[5024,5109],[5112,5117],[5120,5788],[5792,5880],[5888,5909],[5919,5942],[5952,5971],[5984,5996],[5998,6000],[6002,6003],[6016,6109],[6112,6121],[6128,6137],[6144,6169],[6176,6264],[6272,6314],[6320,6389],[6400,6430],[6432,6443],[6448,6459],[6464,6464],[6468,6509],[6512,6516],[6528,6571],[6576,6601],[6608,6618],[6622,6683],[6686,6750],[6752,6780],[6783,6793],[6800,6809],[6816,6829],[6832,6862],[6912,6988],[6992,7038],[7040,7155],[7164,7223],[7227,7241],[7245,7304],[7312,7354],[7357,7367],[7376,7418],[7424,7957],[7960,7965],[7968,8005],[8008,8013],[8016,8023],[8025,8025],[8027,8027],[8029,8029],[8031,8061],[8064,8116],[8118,8132],[8134,8147],[8150,8155],[8157,8175],[8178,8180],[8182,8190],[8192,8292],[8294,8305],[8308,8334],[8336,8348],[8352,8384],[8400,8432],[8448,8587],[8592,9254],[9280,9290],[9312,11123],[11126,11157],[11159,11507],[11513,11557],[11559,11559],[11565,11565],[11568,11623],[11631,11632],[11647,11670],[11680,11686],[11688,11694],[11696,11702],[11704,11710],[11712,11718],[11720,11726],[11728,11734],[11736,11742],[11744,11869],[11904,11929],[11931,12019],[12032,12245],[12272,12283],[12288,12351],[12353,12438],[12441,12543],[12549,12591],[12593,12686],[12688,12771],[12784,12830],[12832,42124],[42128,42182],[42192,42539],[42560,42743],[42752,42954],[42960,42961],[42963,42963],[42965,42969],[42994,43052],[43056,43065],[43072,43127],[43136,43205],[43214,43225],[43232,43347],[43359,43388],[43392,43469],[43471,43481],[43486,43518],[43520,43574],[43584,43597],[43600,43609],[43612,43714],[43739,43766],[43777,43782],[43785,43790],[43793,43798],[43808,43814],[43816,43822],[43824,43883],[43888,44013],[44016,44025],[44032,55203],[55216,55238],[55243,55291],[55296,64109],[64112,64217],[64256,64262],[64275,64279],[64285,64310],[64312,64316],[64318,64318],[64320,64321],[64323,64324],[64326,64450],[64467,64911],[64914,64967],[64975,64975],[65008,65049],[65056,65106],[65108,65126],[65128,65131],[65136,65140],[65142,65276],[65279,65279],[65281,65470],[65474,65479],[65482,65487],[65490,65495],[65498,65500],[65504,65510],[65512,65518],[65529,65533],[65536,65547],[65549,65574],[65576,65594],[65596,65597],[65599,65613],[65616,65629],[65664,65786],[65792,65794],[65799,65843],[65847,65934],[65936,65948],[65952,65952],[66000,66045],[66176,66204],[66208,66256],[66272,66299],[66304,66339],[66349,66378],[66384,66426],[66432,66461],[66463,66499],[66504,66517],[66560,66717],[66720,66729],[66736,66771],[66776,66811],[66816,66855],[66864,66915],[66927,66938],[66940,66954],[66956,66962],[66964,66965],[66967,66977],[66979,66993],[66995,67001],[67003,67004],[67072,67382],[67392,67413],[67424,67431],[67456,67461],[67463,67504],[67506,67514],[67584,67589],[67592,67592],[67594,67637],[67639,67640],[67644,67644],[67647,67669],[67671,67742],[67751,67759],[67808,67826],[67828,67829],[67835,67867],[67871,67897],[67903,67903],[67968,68023],[68028,68047],[68050,68099],[68101,68102],[68108,68115],[68117,68119],[68121,68149],[68152,68154],[68159,68168],[68176,68184],[68192,68255],[68288,68326],[68331,68342],[68352,68405],[68409,68437],[68440,68466],[68472,68497],[68505,68508],[68521,68527],[68608,68680],[68736,68786],[68800,68850],[68858,68903],[68912,68921],[69216,69246],[69248,69289],[69291,69293],[69296,69297],[69373,69415],[69424,69465],[69488,69513],[69552,69579],[69600,69622],[69632,69709],[69714,69749],[69759,69826],[69837,69837],[69840,69864],[69872,69881],[69888,69940],[69942,69959],[69968,70006],[70016,70111],[70113,70132],[70144,70161],[70163,70209],[70272,70278],[70280,70280],[70282,70285],[70287,70301],[70303,70313],[70320,70378],[70384,70393],[70400,70403],[70405,70412],[70415,70416],[70419,70440],[70442,70448],[70450,70451],[70453,70457],[70459,70468],[70471,70472],[70475,70477],[70480,70480],[70487,70487],[70493,70499],[70502,70508],[70512,70516],[70656,70747],[70749,70753],[70784,70855],[70864,70873],[71040,71093],[71096,71133],[71168,71236],[71248,71257],[71264,71276],[71296,71353],[71360,71369],[71424,71450],[71453,71467],[71472,71494],[71680,71739],[71840,71922],[71935,71942],[71945,71945],[71948,71955],[71957,71958],[71960,71989],[71991,71992],[71995,72006],[72016,72025],[72096,72103],[72106,72151],[72154,72164],[72192,72263],[72272,72354],[72368,72440],[72448,72457],[72704,72712],[72714,72758],[72760,72773],[72784,72812],[72816,72847],[72850,72871],[72873,72886],[72960,72966],[72968,72969],[72971,73014],[73018,73018],[73020,73021],[73023,73031],[73040,73049],[73056,73061],[73063,73064],[73066,73102],[73104,73105],[73107,73112],[73120,73129],[73440,73464],[73472,73488],[73490,73530],[73534,73561],[73648,73648],[73664,73713],[73727,74649],[74752,74862],[74864,74868],[74880,75075],[77712,77810],[77824,78933],[82944,83526],[92160,92728],[92736,92766],[92768,92777],[92782,92862],[92864,92873],[92880,92909],[92912,92917],[92928,92997],[93008,93017],[93019,93025],[93027,93047],[93053,93071],[93760,93850],[93952,94026],[94031,94087],[94095,94111],[94176,94180],[94192,94193],[94208,100343],[100352,101589],[101632,101640],[110576,110579],[110581,110587],[110589,110590],[110592,110882],[110898,110898],[110928,110930],[110933,110933],[110948,110951],[110960,111355],[113664,113770],[113776,113788],[113792,113800],[113808,113817],[113820,113827],[118528,118573],[118576,118598],[118608,118723],[118784,119029],[119040,119078],[119081,119274],[119296,119365],[119488,119507],[119520,119539],[119552,119638],[119648,119672],[119808,119892],[119894,119964],[119966,119967],[119970,119970],[119973,119974],[119977,119980],[119982,119993],[119995,119995],[119997,120003],[120005,120069],[120071,120074],[120077,120084],[120086,120092],[120094,120121],[120123,120126],[120128,120132],[120134,120134],[120138,120144],[120146,120485],[120488,120779],[120782,121483],[121499,121503],[121505,121519],[122624,122654],[122661,122666],[122880,122886],[122888,122904],[122907,122913],[122915,122916],[122918,122922],[122928,122989],[123023,123023],[123136,123180],[123184,123197],[123200,123209],[123214,123215],[123536,123566],[123584,123641],[123647,123647],[124112,124153],[124896,124902],[124904,124907],[124909,124910],[124912,124926],[124928,125124],[125127,125142],[125184,125259],[125264,125273],[125278,125279],[126065,126132],[126209,126269],[126464,126467],[126469,126495],[126497,126498],[126500,126500],[126503,126503],[126505,126514],[126516,126519],[126521,126521],[126523,126523],[126530,126530],[126535,126535],[126537,126537],[126539,126539],[126541,126543],[126545,126546],[126548,126548],[126551,126551],[126553,126553],[126555,126555],[126557,126557],[126559,126559],[126561,126562],[126564,126564],[126567,126570],[126572,126578],[126580,126583],[126585,126588],[126590,126590],[126592,126601],[126603,126619],[126625,126627],[126629,126633],[126635,126651],[126704,126705],[126976,127019],[127024,127123],[127136,127150],[127153,127167],[127169,127183],[127185,127221],[127232,127405],[127462,127490],[127504,127547],[127552,127560],[127568,127569],[127584,127589],[127744,128727],[128732,128748],[128752,128764],[128768,128886],[128891,128985],[128992,129003],[129008,129008],[129024,129035],[129040,129095],[129104,129113],[129120,129159],[129168,129197],[129200,129201],[129280,129619],[129632,129645],[129648,129660],[129664,129672],[129680,129725],[129727,129733],[129742,129755],[129760,129768],[129776,129784],[129792,129938],[129940,129994],[130032,130041],[131072,173791],[173824,177977],[177984,178205],[178208,183969],[183984,191456],[194560,195101],[196608,201546],[201552,205743],[917505,917505],[917536,917631],[917760,917999],[983040,1048573],[1048576,1114109]],"format":[[173,173],[1536,1541],[1564,1564],[1757,1757],[1807,1807],[2192,2193],[2274,2274],[6158,6158],[8203,8207],[8234,8238],[8288,8292],[8294,8303],[65279,65279],[65529,65531],[69821,69821],[69837,69837],[78896,78911],[113824,113827],[119155,119162],[917505,917505],[917536,917631]],"combiningClass":[[768,230],[769,230],[770,230],[771,230],[772,230],[773,230],[774,230],[775,230],[776,230],[777,230],[778,230],[779,230],[780,230],[781,230],[782,230],[783,230],[784,230],[785,230],[786,230],[787,230],[788,230],[789,232],[790,220],[791,220],[792,220],[793,220],[794,232],[795,216],[796,220],[797,220],[798,220],[799,220],[800,220],[801,202],[802,202],[803,220],[804,220],[805,220],[806,220],[807,202],[808,202],[809,220],[810,220],[811,220],[812,220],[813,220],[814,220],[815,220],[816,220],[817,220],[818,220],[819,220],[820,1],[821,1],[822,1],[823,1],[824,1],[825,220],[826,220],[827,220],[828,220],[829,230],[830,230],[831,230],[832,230],[833,230],[834,230],[835,230],[836,230],[837,240],[838,230],[839,220],[840,220],[841,220],[842,230],[843,230],[844,230],[845,220],[846,220],[848,230],[849,230],[850,230],[851,220],[852,220],[853,220],[854,220],[855,230],[856,232],[857,220],[858,220],[859,230],[860,233],[861,234],[862,234],[863,233],[864,234],[865,234],[866,233],[867,230],[868,230],[869,230],[870,230],[871,230],[872,230],[873,230],[874,230],[875,230],[876,230],[877,230],[878,230],[879,230],[1155,230],[1156,230],[1157,230],[1158,230],[1159,230],[1425,220],[1426,230],[1427,230],[1428,230],[1429,230],[1430,220],[1431,230],[1432,230],[1433,230],[1434,222],[1435,220],[1436,230],[1437,230],[1438,230],[1439,230],[1440,230],[1441,230],[1442,220],[1443,220],[1444,220],[1445,220],[1446,220],[1447,220],[1448,230],[1449,230],[1450,220],[1451,230],[1452,230],[1453,222],[1454,228],[1455,230],[1456,10],[1457,11],[1458,12],[1459,13],[1460,14],[1461,15],[1462,16],[1463,17],[1464,18],[1465,19],[1466,19],[1467,20],[1468,21],[1469,22],[1471,23],[1473,24],[1474,25],[1476,230],[1477,220],[1479,18],[1552,230],[1553,230],[1554,230],[1555,230],[1556,230],[1557,230],[1558,230],[1559,230],[1560,30],[1561,31],[1562,32],[1611,27],[1612,28],[1613,29],[1614,30],[1615,31],[1616,32],[1617,33],[1618,34],[1619,230],[1620,230],[1621,220],[1622,220],[1623,230],[1624,230],[1625,230],[1626,230],[1627,230],[1628,220],[1629,230],[1630,230],[1631,220],[1648,35],[1750,230],[1751,230],[1752,230],[1753,230],[1754,230],[1755,230],[1756,230],[1759,230],[1760,230],[1761,230],[1762,230],[1763,220],[1764,230],[1767,230],[1768,230],[1770,220],[1771,230],[1772,230],[1773,220],[1809,36],[1840,230],[1841,220],[1842,230],[1843,230],[1844,220],[1845,230],[1846,230],[1847,220],[1848,220],[1849,220],[1850,230],[1851,220],[1852,220],[1853,230],[1854,220],[1855,230],[1856,230],[1857,230],[1858,220],[1859,230],[1860,220],[1861,230],[1862,220],[1863,230],[1864,220],[1865,230],[1866,230],[2027,230],[2028,230],[2029,230],[2030,230],[2031,230],[2032,230],[2033,230],[2034,220],[2035,230],[2045,220],[2070,230],[2071,230],[2072,230],[2073,230],[2075,230],[2076,230],[2077,230],[2078,230],[2079,230],[2080,230],[2081,230],[2082,230],[2083,230],[2085,230],[2086,230],[2087,230],[2089,230],[2090,230],[2091,230],[2092,230],[2093,230],[2137,220],[2138,220],[2139,220],[2200,230],[2201,220],[2202,220],[2203,220],[2204,230],[2205,230],[2206,230],[2207,230],[2250,230],[2251,230],[2252,230],[2253,230],[2254,230],[2255,220],[2256,220],[2257,220],[2258,220],[2259,220],[2260,230],[2261,230],[2262,230],[2263,230],[2264,230],[2265,230],[2266,230],[2267,230],[2268,230],[2269,230],[2270,230],[2271,230],[2272,230],[2273,230],[2275,220],[2276,230],[2277,230],[2278,220],[2279,230],[2280,230],[2281,220],[2282,230],[2283,230],[2284,230],[2285,220],[2286,220],[2287,220],[2288,27],[2289,28],[2290,29],[2291,230],[2292,230],[2293,230],[2294,220],[2295,230],[2296,230],[2297,220],[2298,220],[2299,230],[2300,230],[2301,230],[2302,230],[2303,230],[2364,7],[2381,9],[2385,230],[2386,220],[2387,230],[2388,230],[2492,7],[2509,9],[2558,230],[2620,7],[2637,9],[2748,7],[2765,9],[2876,7],[2893,9],[3021,9],[3132,7],[3149,9],[3157,84],[3158,91],[3260,7],[3277,9],[3387,9],[3388,9],[3405,9],[3530,9],[3640,103],[3641,103],[3642,9],[3656,107],[3657,107],[3658,107],[3659,107],[3768,118],[3769,118],[3770,9],[3784,122],[3785,122],[3786,122],[3787,122],[3864,220],[3865,220],[3893,220],[3895,220],[3897,216],[3953,129],[3954,130],[3956,132],[3962,130],[3963,130],[3964,130],[3965,130],[3968,130],[3970,230],[3971,230],[3972,9],[3974,230],[3975,230],[4038,220],[4151,7],[4153,9],[4154,9],[4237,220],[4957,230],[4958,230],[4959,230],[5908,9],[5909,9],[5940,9],[6098,9],[6109,230],[6313,228],[6457,222],[6458,230],[6459,220],[6679,230],[6680,220],[6752,9],[6773,230],[6774,230],[6775,230],[6776,230],[6777,230],[6778,230],[6779,230],[6780,230],[6783,220],[6832,230],[6833,230],[6834,230],[6835,230],[6836,230],[6837,220],[6838,220],[6839,220],[6840,220],[6841,220],[6842,220],[6843,230],[6844,230],[6845,220],[6847,220],[6848,220],[6849,230],[6850,230],[6851,220],[6852,220],[6853,230],[6854,230],[6855,230],[6856,230],[6857,230],[6858,220],[6859,230],[6860,230],[6861,230],[6862,230],[6964,7],[6980,9],[7019,230],[7020,220],[7021,230],[7022,230],[7023,230],[7024,230],[7025,230],[7026,230],[7027,230],[7082,9],[7083,9],[7142,7],[7154,9],[7155,9],[7223,7],[7376,230],[7377,230],[7378,230],[7380,1],[7381,220],[7382,220],[7383,220],[7384,220],[7385,220],[7386,230],[7387,230],[7388,220],[7389,220],[7390,220],[7391,220],[7392,230],[7394,1],[7395,1],[7396,1],[7397,1],[7398,1],[7399,1],[7400,1],[7405,220],[7412,230],[7416,230],[7417,230],[7616,230],[7617,230],[7618,220],[7619,230],[7620,230],[7621,230],[7622,230],[7623,230],[7624,230],[7625,230],[7626,220],[7627,230],[7628,230],[7629,234],[7630,214],[7631,220],[7632,202],[7633,230],[7634,230],[7635,230],[7636,230],[7637,230],[7638,230],[7639,230],[7640,230],[7641,230],[7642,230],[7643,230],[7644,230],[7645,230],[7646,230],[7647,230],[7648,230],[7649,230],[7650,230],[7651,230],[7652,230],[7653,230],[7654,230],[7655,230],[7656,230],[7657,230],[7658,230],[7659,230],[7660,230],[7661,230],[7662,230],[7663,230],[7664,230],[7665,230],[7666,230],[7667,230],[7668,230],[7669,230],[7670,232],[7671,228],[7672,228],[7673,220],[7674,218],[7675,230],[7676,233],[7677,220],[7678,230],[7679,220],[8400,230],[8401,230],[8402,1],[8403,1],[8404,230],[8405,230],[8406,230],[8407,230],[8408,1],[8409,1],[8410,1],[8411,230],[8412,230],[8417,230],[8421,1],[8422,1],[8423,230],[8424,220],[8425,230],[8426,1],[8427,1],[8428,220],[8429,220],[8430,220],[8431,220],[8432,230],[11503,230],[11504,230],[11505,230],[11647,9],[11744,230],[11745,230],[11746,230],[11747,230],[11748,230],[11749,230],[11750,230],[11751,230],[11752,230],[11753,230],[11754,230],[11755,230],[11756,230],[11757,230],[11758,230],[11759,230],[11760,230],[11761,230],[11762,230],[11763,230],[11764,230],[11765,230],[11766,230],[11767,230],[11768,230],[11769,230],[11770,230],[11771,230],[11772,230],[11773,230],[11774,230],[11775,230],[12330,218],[12331,228],[12332,232],[12333,222],[12334,224],[12335,224],[12441,8],[12442,8],[42607,230],[42612,230],[42613,230],[42614,230],[42615,230],[42616,230],[42617,230],[42618,230],[42619,230],[42620,230],[42621,230],[42654,230],[42655,230],[42736,230],[42737,230],[43014,9],[43052,9],[43204,9],[43232,230],[43233,230],[43234,230],[43235,230],[43236,230],[43237,230],[43238,230],[43239,230],[43240,230],[43241,230],[43242,230],[43243,230],[43244,230],[43245,230],[43246,230],[43247,230],[43248,230],[43249,230],[43307,220],[43308,220],[43309,220],[43347,9],[43443,7],[43456,9],[43696,230],[43698,230],[43699,230],[43700,220],[43703,230],[43704,230],[43710,230],[43711,230],[43713,230],[43766,9],[44013,9],[64286,26],[65056,230],[65057,230],[65058,230],[65059,230],[65060,230],[65061,230],[65062,230],[65063,220],[65064,220],[65065,220],[65066,220],[65067,220],[65068,220],[65069,220],[65070,230],[65071,230],[66045,220],[66272,220],[66422,230],[66423,230],[66424,230],[66425,230],[66426,230],[68109,220],[68111,230],[68152,230],[68153,1],[68154,220],[68159,9],[68325,230],[68326,220],[68900,230],[68901,230],[68902,230],[68903,230],[69291,230],[69292,230],[69373,220],[69374,220],[69375,220],[69446,220],[69447,220],[69448,230],[69449,230],[69450,230],[69451,220],[69452,230],[69453,220],[69454,220],[69455,220],[69456,220],[69506,230],[69507,220],[69508,230],[69509,220],[69702,9],[69744,9],[69759,9],[69817,9],[69818,7],[69888,230],[69889,230],[69890,230],[69939,9],[69940,9],[70003,7],[70080,9],[70090,7],[70197,9],[70198,7],[70377,7],[70378,9],[70459,7],[70460,7],[70477,9],[70502,230],[70503,230],[70504,230],[70505,230],[70506,230],[70507,230],[70508,230],[70512,230],[70513,230],[70514,230],[70515,230],[70516,230],[70722,9],[70726,7],[70750,230],[70850,9],[70851,7],[71103,9],[71104,7],[71231,9],[71350,9],[71351,7],[71467,9],[71737,9],[71738,7],[71997,9],[71998,9],[72003,7],[72160,9],[72244,9],[72263,9],[72345,9],[72767,9],[73026,7],[73028,9],[73029,9],[73111,9],[73537,9],[73538,9],[92912,1],[92913,1],[92914,1],[92915,1],[92916,1],[92976,230],[92977,230],[92978,230],[92979,230],[92980,230],[92981,230],[92982,230],[94192,6],[94193,6],[113822,1],[119141,216],[119142,216],[119143,1],[119144,1],[119145,1],[119149,226],[119150,216],[119151,216],[119152,216],[119153,216],[119154,216],[119163,220],[119164,220],[119165,220],[119166,220],[119167,220],[119168,220],[119169,220],[119170,220],[119173,230],[119174,230],[119175,230],[119176,230],[119177,230],[119178,220],[119179,220],[119210,230],[119211,230],[119212,230],[119213,230],[119362,230],[119363,230],[119364,230],[122880,230],[122881,230],[122882,230],[122883,230],[122884,230],[122885,230],[122886,230],[122888,230],[122889,230],[122890,230],[122891,230],[122892,230],[122893,230],[122894,230],[122895,230],[122896,230],[122897,230],[122898,230],[122899,230],[122900,230],[122901,230],[122902,230],[122903,230],[122904,230],[122907,230],[122908,230],[122909,230],[122910,230],[122911,230],[122912,230],[122913,230],[122915,230],[122916,230],[122918,230],[122919,230],[122920,230],[122921,230],[122922,230],[123023,230],[123184,230],[123185,230],[123186,230],[123187,230],[123188,230],[123189,230],[123190,230],[123566,230],[123628,230],[123629,230],[123630,230],[123631,230],[124140,232],[124141,232],[124142,220],[124143,230],[125136,220],[125137,220],[125138,220],[125139,220],[125140,220],[125141,220],[125142,220],[125252,230],[125253,230],[125254,230],[125255,230],[125256,230],[125257,230],[125258,7]],"decompositions":[[192,65,768],[193,65,769],[194,65,770],[195,65,771],[196,65,776],[197,65,778],[199,67,807],[200,69,768],[201,69,769],[202,69,770],[203,69,776],[204,73,768],[205,73,769],[206,73,770],[207,73,776],[209,78,771],[210,79,768],[211,79,769],[212,79,770],[213,79,771],[214,79,776],[217,85,768],[218,85,769],[219,85,770],[220,85,776],[221,89,769],[224,97,768],[225,97,769],[226,97,770],[227,97,771],[228,97,776],[229,97,778],[231,99,807],[232,101,768],[233,101,769],[234,101,770],[235,101,776],[236,105,768],[237,105,769],[238,105,770],[239,105,776],[241,110,771],[242,111,768],[243,111,769],[244,111,770],[245,111,771],[246,111,776],[249,117,768],[250,117,769],[251,117,770],[252,117,776],[253,121,769],[255,121,776],[256,65,772],[257,97,772],[258,65,774],[259,97,774],[260,65,808],[261,97,808],[262,67,769],[263,99,769],[264,67,770],[265,99,770],[266,67,775],[267,99,775],[268,67,780],[269,99,780],[270,68,780],[271,100,780],[274,69,772],[275,101,772],[276,69,774],[277,101,774],[278,69,775],[279,101,775],[280,69,808],[281,101,808],[282,69,780],[283,101,780],[284,71,770],[285,103,770],[286,71,774],[287,103,774],[288,71,775],[289,103,775],[290,71,807],[291,103,807],[292,72,770],[293,104,770],[296,73,771],[297,105,771],[298,73,772],[299,105,772],[300,73,774],[301,105,774],[302,73,808],[303,105,808],[304,73,775],[308,74,770],[309,106,770],[310,75,807],[311,107,807],[313,76,769],[314,108,769],[315,76,807],[316,108,807],[317,76,780],[318,108,780],[323,78,769],[324,110,769],[325,78,807],[326,110,807],[327,78,780],[328,110,780],[332,79,772],[333,111,772],[334,79,774],[335,111,774],[336,79,779],[337,111,779],[340,82,769],[341,114,769],[342,82,807],[343,114,807],[344,82,780],[345,114,780],[346,83,769],[347,115,769],[348,83,770],[349,115,770],[350,83,807],[351,115,807],[352,83,780],[353,115,780],[354,84,807],[355,116,807],[356,84,780],[357,116,780],[360,85,771],[361,117,771],[362,85,772],[363,117,772],[364,85,774],[365,117,774],[366,85,778],[367,117,778],[368,85,779],[369,117,779],[370,85,808],[371,117,808],[372,87,770],[373,119,770],[374,89,770],[375,121,770],[376,89,776],[377,90,769],[378,122,769],[379,90,775],[380,122,775],[381,90,780],[382,122,780],[416,79,795],[417,111,795],[431,85,795],[432,117,795],[461,65,780],[462,97,780],[463,73,780],[464,105,780],[465,79,780],[466,111,780],[467,85,780],[468,117,780],[469,220,772],[470,252,772],[471,220,769],[472,252,769],[473,220,780],[474,252,780],[475,220,768],[476,252,768],[478,196,772],[479,228,772],[480,550,772],[481,551,772],[482,198,772],[483,230,772],[486,71,780],[487,103,780],[488,75,780],[489,107,780],[490,79,808],[491,111,808],[492,490,772],[493,491,772],[494,439,780],[495,658,780],[496,106,780],[500,71,769],[501,103,769],[504,78,768],[505,110,768],[506,197,769],[507,229,769],[508,198,769],[509,230,769],[510,216,769],[511,248,769],[512,65,783],[513,97,783],[514,65,785],[515,97,785],[516,69,783],[517,101,783],[518,69,785],[519,101,785],[520,73,783],[521,105,783],[522,73,785],[523,105,785],[524,79,783],[525,111,783],[526,79,785],[527,111,785],[528,82,783],[529,114,783],[530,82,785],[531,114,785],[532,85,783],[533,117,783],[534,85,785],[535,117,785],[536,83,806],[537,115,806],[538,84,806],[539,116,806],[542,72,780],[543,104,780],[550,65,775],[551,97,775],[552,69,807],[553,101,807],[554,214,772],[555,246,772],[556,213,772],[557,245,772],[558,79,775],[559,111,775],[560,558,772],[561,559,772],[562,89,772],[563,121,772],[832,768],[833,769],[835,787],[836,776,769],[884,697],[894,59],[901,168,769],[902,913,769],[903,183],[904,917,769],[905,919,769],[906,921,769],[908,927,769],[910,933,769],[911,937,769],[912,970,769],[938,921,776],[939,933,776],[940,945,769],[941,949,769],[942,951,769],[943,953,769],[944,971,769],[970,953,776],[971,965,776],[972,959,769],[973,965,769],[974,969,769],[979,978,769],[980,978,776],[1024,1045,768],[1025,1045,776],[1027,1043,769],[1031,1030,776],[1036,1050,769],[1037,1048,768],[1038,1059,774],[1049,1048,774],[1081,1080,774],[1104,1077,768],[1105,1077,776],[1107,1075,769],[1111,1110,776],[1116,1082,769],[1117,1080,768],[1118,1091,774],[1142,1140,783],[1143,1141,783],[1217,1046,774],[1218,1078,774],[1232,1040,774],[1233,1072,774],[1234,1040,776],[1235,1072,776],[1238,1045,774],[1239,1077,774],[1242,1240,776],[1243,1241,776],[1244,1046,776],[1245,1078,776],[1246,1047,776],[1247,1079,776],[1250,1048,772],[1251,1080,772],[1252,1048,776],[1253,1080,776],[1254,1054,776],[1255,1086,776],[1258,1256,776],[1259,1257,776],[1260,1069,776],[1261,1101,776],[1262,1059,772],[1263,1091,772],[1264,1059,776],[1265,1091,776],[1266,1059,779],[1267,1091,779],[1268,1063,776],[1269,1095,776],[1272,1067,776],[1273,1099,776],[1570,1575,1619],[1571,1575,1620],[1572,1608,1620],[1573,1575,1621],[1574,1610,1620],[1728,1749,1620],[1730,1729,1620],[1747,1746,1620],[2345,2344,2364],[2353,2352,2364],[2356,2355,2364],[2392,2325,2364],[2393,2326,2364],[2394,2327,2364],[2395,2332,2364],[2396,2337,2364],[2397,2338,2364],[2398,2347,2364],[2399,2351,2364],[2507,2503,2494],[2508,2503,2519],[2524,2465,2492],[2525,2466,2492],[2527,2479,2492],[2611,2610,2620],[2614,2616,2620],[2649,2582,2620],[2650,2583,2620],[2651,2588,2620],[2654,2603,2620],[2888,2887,2902],[2891,2887,2878],[2892,2887,2903],[2908,2849,2876],[2909,2850,2876],[2964,2962,3031],[3018,3014,3006],[3019,3015,3006],[3020,3014,3031],[3144,3142,3158],[3264,3263,3285],[3271,3270,3285],[3272,3270,3286],[3274,3270,3266],[3275,3274,3285],[3402,3398,3390],[3403,3399,3390],[3404,3398,3415],[3546,3545,3530],[3548,3545,3535],[3549,3548,3530],[3550,3545,3551],[3907,3906,4023],[3917,3916,4023],[3922,3921,4023],[3927,3926,4023],[3932,3931,4023],[3945,3904,4021],[3955,3953,3954],[3957,3953,3956],[3958,4018,3968],[3960,4019,3968],[3969,3953,3968],[3987,3986,4023],[3997,3996,4023],[4002,4001,4023],[4007,4006,4023],[4012,4011,4023],[4025,3984,4021],[4134,4133,4142],[6918,6917,6965],[6920,6919,6965],[6922,6921,6965],[6924,6923,6965],[6926,6925,6965],[6930,6929,6965],[6971,6970,6965],[6973,6972,6965],[6976,6974,6965],[6977,6975,6965],[6979,6978,6965],[7680,65,805],[7681,97,805],[7682,66,775],[7683,98,775],[7684,66,803],[7685,98,803],[7686,66,817],[7687,98,817],[7688,199,769],[7689,231,769],[7690,68,775],[7691,100,775],[7692,68,803],[7693,100,803],[7694,68,817],[7695,100,817],[7696,68,807],[7697,100,807],[7698,68,813],[7699,100,813],[7700,274,768],[7701,275,768],[7702,274,769],[7703,275,769],[7704,69,813],[7705,101,813],[7706,69,816],[7707,101,816],[7708,552,774],[7709,553,774],[7710,70,775],[7711,102,775],[7712,71,772],[7713,103,772],[7714,72,775],[7715,104,775],[7716,72,803],[7717,104,803],[7718,72,776],[7719,104,776],[7720,72,807],[7721,104,807],[7722,72,814],[7723,104,814],[7724,73,816],[7725,105,816],[7726,207,769],[7727,239,769],[7728,75,769],[7729,107,769],[7730,75,803],[7731,107,803],[7732,75,817],[7733,107,817],[7734,76,803],[7735,108,803],[7736,7734,772],[7737,7735,772],[7738,76,817],[7739,108,817],[7740,76,813],[7741,108,813],[7742,77,769],[7743,109,769],[7744,77,775],[7745,109,775],[7746,77,803],[7747,109,803],[7748,78,775],[7749,110,775],[7750,78,803],[7751,110,803],[7752,78,817],[7753,110,817],[7754,78,813],[7755,110,813],[7756,213,769],[7757,245,769],[7758,213,776],[7759,245,776],[7760,332,768],[7761,333,768],[7762,332,769],[7763,333,769],[7764,80,769],[7765,112,769],[7766,80,775],[7767,112,775],[7768,82,775],[7769,114,775],[7770,82,803],[7771,114,803],[7772,7770,772],[7773,7771,772],[7774,82,817],[7775,114,817],[7776,83,775],[7777,115,775],[7778,83,803],[7779,115,803],[7780,346,775],[7781,347,775],[7782,352,775],[7783,353,775],[7784,7778,775],[7785,7779,775],[7786,84,775],[7787,116,775],[7788,84,803],[7789,116,803],[7790,84,817],[7791,116,817],[7792,84,813],[7793,116,813],[7794,85,804],[7795,117,804],[7796,85,816],[7797,117,816],[7798,85,813],[7799,117,813],[7800,360,769],[7801,361,769],[7802,362,776],[7803,363,776],[7804,86,771],[7805,118,771],[7806,86,803],[7807,118,803],[7808,87,768],[7809,119,768],[7810,87,769],[7811,119,769],[7812,87,776],[7813,119,776],[7814,87,775],[7815,119,775],[7816,87,803],[7817,119,803],[7818,88,775],[7819,120,775],[7820,88,776],[7821,120,776],[7822,89,775],[7823,121,775],[7824,90,770],[7825,122,770],[7826,90,803],[7827,122,803],[7828,90,817],[7829,122,817],[7830,104,817],[7831,116,776],[7832,119,778],[7833,121,778],[7835,383,775],[7840,65,803],[7841,97,803],[7842,65,777],[7843,97,777],[7844,194,769],[7845,226,769],[7846,194,768],[7847,226,768],[7848,194,777],[7849,226,777],[7850,194,771],[7851,226,771],[7852,7840,770],[7853,7841,770],[7854,258,769],[7855,259,769],[7856,258,768],[7857,259,768],[7858,258,777],[7859,259,777],[7860,258,771],[7861,259,771],[7862,7840,774],[7863,7841,774],[7864,69,803],[7865,101,803],[7866,69,777],[7867,101,777],[7868,69,771],[7869,101,771],[7870,202,769],[7871,234,769],[7872,202,768],[7873,234,768],[7874,202,777],[7875,234,777],[7876,202,771],[7877,234,771],[7878,7864,770],[7879,7865,770],[7880,73,777],[7881,105,777],[7882,73,803],[7883,105,803],[7884,79,803],[7885,111,803],[7886,79,777],[7887,111,777],[7888,212,769],[7889,244,769],[7890,212,768],[7891,244,768],[7892,212,777],[7893,244,777],[7894,212,771],[7895,244,771],[7896,7884,770],[7897,7885,770],[7898,416,769],[7899,417,769],[7900,416,768],[7901,417,768],[7902,416,777],[7903,417,777],[7904,416,771],[7905,417,771],[7906,416,803],[7907,417,803],[7908,85,803],[7909,117,803],[7910,85,777],[7911,117,777],[7912,431,769],[7913,432,769],[7914,431,768],[7915,432,768],[7916,431,777],[7917,432,777],[7918,431,771],[7919,432,771],[7920,431,803],[7921,432,803],[7922,89,768],[7923,121,768],[7924,89,803],[7925,121,803],[7926,89,777],[7927,121,777],[7928,89,771],[7929,121,771],[7936,945,787],[7937,945,788],[7938,7936,768],[7939,7937,768],[7940,7936,769],[7941,7937,769],[7942,7936,834],[7943,7937,834],[7944,913,787],[7945,913,788],[7946,7944,768],[7947,7945,768],[7948,7944,769],[7949,7945,769],[7950,7944,834],[7951,7945,834],[7952,949,787],[7953,949,788],[7954,7952,768],[7955,7953,768],[7956,7952,769],[7957,7953,769],[7960,917,787],[7961,917,788],[7962,7960,768],[7963,7961,768],[7964,7960,769],[7965,7961,769],[7968,951,787],[7969,951,788],[7970,7968,768],[7971,7969,768],[7972,7968,769],[7973,7969,769],[7974,7968,834],[7975,7969,834],[7976,919,787],[7977,919,788],[7978,7976,768],[7979,7977,768],[7980,7976,769],[7981,7977,769],[7982,7976,834],[7983,7977,834],[7984,953,787],[7985,953,788],[7986,7984,768],[7987,7985,768],[7988,7984,769],[7989,7985,769],[7990,7984,834],[7991,7985,834],[7992,921,787],[7993,921,788],[7994,7992,768],[7995,7993,768],[7996,7992,769],[7997,7993,769],[7998,7992,834],[7999,7993,834],[8000,959,787],[8001,959,788],[8002,8000,768],[8003,8001,768],[8004,8000,769],[8005,8001,769],[8008,927,787],[8009,927,788],[8010,8008,768],[8011,8009,768],[8012,8008,769],[8013,8009,769],[8016,965,787],[8017,965,788],[8018,8016,768],[8019,8017,768],[8020,8016,769],[8021,8017,769],[8022,8016,834],[8023,8017,834],[8025,933,788],[8027,8025,768],[8029,8025,769],[8031,8025,834],[8032,969,787],[8033,969,788],[8034,8032,768],[8035,8033,768],[8036,8032,769],[8037,8033,769],[8038,8032,834],[8039,8033,834],[8040,937,787],[8041,937,788],[8042,8040,768],[8043,8041,768],[8044,8040,769],[8045,8041,769],[8046,8040,834],[8047,8041,834],[8048,945,768],[8049,945,769],[8050,949,768],[8051,949,769],[8052,951,768],[8053,951,769],[8054,953,768],[8055,953,769],[8056,959,768],[8057,959,769],[8058,965,768],[8059,965,769],[8060,969,768],[8061,969,769],[8064,7936,837],[8065,7937,837],[8066,7938,837],[8067,7939,837],[8068,7940,837],[8069,7941,837],[8070,7942,837],[8071,7943,837],[8072,7944,837],[8073,7945,837],[8074,7946,837],[8075,7947,837],[8076,7948,837],[8077,7949,837],[8078,7950,837],[8079,7951,837],[8080,7968,837],[8081,7969,837],[8082,7970,837],[8083,7971,837],[8084,7972,837],[8085,7973,837],[8086,7974,837],[8087,7975,837],[8088,7976,837],[8089,7977,837],[8090,7978,837],[8091,7979,837],[8092,7980,837],[8093,7981,837],[8094,7982,837],[8095,7983,837],[8096,8032,837],[8097,8033,837],[8098,8034,837],[8099,8035,837],[8100,8036,837],[8101,8037,837],[8102,8038,837],[8103,8039,837],[8104,8040,837],[8105,8041,837],[8106,8042,837],[8107,8043,837],[8108,8044,837],[8109,8045,837],[8110,8046,837],[8111,8047,837],[8112,945,774],[8113,945,772],[8114,8048,837],[8115,945,837],[8116,940,837],[8118,945,834],[8119,8118,837],[8120,913,774],[8121,913,772],[8122,913,768],[8123,913,769],[8124,913,837],[8126,953],[8129,168,834],[8130,8052,837],[8131,951,837],[8132,942,837],[8134,951,834],[8135,8134,837],[8136,917,768],[8137,917,769],[8138,919,768],[8139,919,769],[8140,919,837],[8141,8127,768],[8142,8127,769],[8143,8127,834],[8144,953,774],[8145,953,772],[8146,970,768],[8147,953,776,769],[8150,953,834],[8151,970,834],[8152,921,774],[8153,921,772],[8154,921,768],[8155,921,769],[8157,8190,768],[8158,8190,769],[8159,8190,834],[8160,965,774],[8161,965,772],[8162,971,768],[8163,965,776,769],[8164,961,787],[8165,961,788],[8166,965,834],[8167,971,834],[8168,933,774],[8169,933,772],[8170,933,768],[8171,933,769],[8172,929,788],[8173,168,768],[8174,168,769],[8175,96],[8178,8060,837],[8179,969,837],[8180,974,837],[8182,969,834],[8183,8182,837],[8184,927,768],[8185,927,769],[8186,937,768],[8187,937,769],[8188,937,837],[8189,180],[8192,8194],[8193,8195],[8486,937],[8490,75],[8491,65,778],[8602,8592,824],[8603,8594,824],[8622,8596,824],[8653,8656,824],[8654,8660,824],[8655,8658,824],[8708,8707,824],[8713,8712,824],[8716,8715,824],[8740,8739,824],[8742,8741,824],[8769,8764,824],[8772,8771,824],[8775,8773,824],[8777,8776,824],[8800,61,824],[8802,8801,824],[8813,8781,824],[8814,60,824],[8815,62,824],[8816,8804,824],[8817,8805,824],[8820,8818,824],[8821,8819,824],[8824,8822,824],[8825,8823,824],[8832,8826,824],[8833,8827,824],[8836,8834,824],[8837,8835,824],[8840,8838,824],[8841,8839,824],[8876,8866,824],[8877,8872,824],[8878,8873,824],[8879,8875,824],[8928,8828,824],[8929,8829,824],[8930,8849,824],[8931,8850,824],[8938,8882,824],[8939,8883,824],[8940,8884,824],[8941,8885,824],[9001,12296],[9002,12297],[10972,10973,824],[12364,12363,12441],[12366,12365,12441],[12368,12367,12441],[12370,12369,12441],[12372,12371,12441],[12374,12373,12441],[12376,12375,12441],[12378,12377,12441],[12380,12379,12441],[12382,12381,12441],[12384,12383,12441],[12386,12385,12441],[12389,12388,12441],[12391,12390,12441],[12393,12392,12441],[12400,12399,12441],[12401,12399,12442],[12403,12402,12441],[12404,12402,12442],[12406,12405,12441],[12407,12405,12442],[12409,12408,12441],[12410,12408,12442],[12412,12411,12441],[12413,12411,12442],[12436,12358,12441],[12446,12445,12441],[12460,12459,12441],[12462,12461,12441],[12464,12463,12441],[12466,12465,12441],[12468,12467,12441],[12470,12469,12441],[12472,12471,12441],[12474,12473,12441],[12476,12475,12441],[12478,12477,12441],[12480,12479,12441],[12482,12481,12441],[12485,12484,12441],[12487,12486,12441],[12489,12488,12441],[12496,12495,12441],[12497,12495,12442],[12499,12498,12441],[12500,12498,12442],[12502,12501,12441],[12503,12501,12442],[12505,12504,12441],[12506,12504,12442],[12508,12507,12441],[12509,12507,12442],[12532,12454,12441],[12535,12527,12441],[12536,12528,12441],[12537,12529,12441],[12538,12530,12441],[12542,12541,12441],[63744,35912],[63745,26356],[63746,36554],[63747,36040],[63748,28369],[63749,20018],[63750,21477],[63751,40860],[63752,40860],[63753,22865],[63754,37329],[63755,21895],[63756,22856],[63757,25078],[63758,30313],[63759,32645],[63760,34367],[63761,34746],[63762,35064],[63763,37007],[63764,27138],[63765,27931],[63766,28889],[63767,29662],[63768,33853],[63769,37226],[63770,39409],[63771,20098],[63772,21365],[63773,27396],[63774,29211],[63775,34349],[63776,40478],[63777,23888],[63778,28651],[63779,34253],[63780,35172],[63781,25289],[63782,33240],[63783,34847],[63784,24266],[63785,26391],[63786,28010],[63787,29436],[63788,37070],[63789,20358],[63790,20919],[63791,21214],[63792,25796],[63793,27347],[63794,29200],[63795,30439],[63796,32769],[63797,34310],[63798,34396],[63799,36335],[63800,38706],[63801,39791],[63802,40442],[63803,30860],[63804,31103],[63805,32160],[63806,33737],[63807,37636],[63808,40575],[63809,35542],[63810,22751],[63811,24324],[63812,31840],[63813,32894],[63814,29282],[63815,30922],[63816,36034],[63817,38647],[63818,22744],[63819,23650],[63820,27155],[63821,28122],[63822,28431],[63823,32047],[63824,32311],[63825,38475],[63826,21202],[63827,32907],[63828,20956],[63829,20940],[63830,31260],[63831,32190],[63832,33777],[63833,38517],[63834,35712],[63835,25295],[63836,27138],[63837,35582],[63838,20025],[63839,23527],[63840,24594],[63841,29575],[63842,30064],[63843,21271],[63844,30971],[63845,20415],[63846,24489],[63847,19981],[63848,27852],[63849,25976],[63850,32034],[63851,21443],[63852,22622],[63853,30465],[63854,33865],[63855,35498],[63856,27578],[63857,36784],[63858,27784],[63859,25342],[63860,33509],[63861,25504],[63862,30053],[63863,20142],[63864,20841],[63865,20937],[63866,26753],[63867,31975],[63868,33391],[63869,35538],[63870,37327],[63871,21237],[63872,21570],[63873,22899],[63874,24300],[63875,26053],[63876,28670],[63877,31018],[63878,38317],[63879,39530],[63880,40599],[63881,40654],[63882,21147],[63883,26310],[63884,27511],[63885,36706],[63886,24180],[63887,24976],[63888,25088],[63889,25754],[63890,28451],[63891,29001],[63892,29833],[63893,31178],[63894,32244],[63895,32879],[63896,36646],[63897,34030],[63898,36899],[63899,37706],[63900,21015],[63901,21155],[63902,21693],[63903,28872],[63904,35010],[63905,35498],[63906,24265],[63907,24565],[63908,25467],[63909,27566],[63910,31806],[63911,29557],[63912,20196],[63913,22265],[63914,23527],[63915,23994],[63916,24604],[63917,29618],[63918,29801],[63919,32666],[63920,32838],[63921,37428],[63922,38646],[63923,38728],[63924,38936],[63925,20363],[63926,31150],[63927,37300],[63928,38584],[63929,24801],[63930,20102],[63931,20698],[63932,23534],[63933,23615],[63934,26009],[63935,27138],[63936,29134],[63937,30274],[63938,34044],[63939,36988],[63940,40845],[63941,26248],[63942,38446],[63943,21129],[63944,26491],[63945,26611],[63946,27969],[63947,28316],[63948,29705],[63949,30041],[63950,30827],[63951,32016],[63952,39006],[63953,20845],[63954,25134],[63955,38520],[63956,20523],[63957,23833],[63958,28138],[63959,36650],[63960,24459],[63961,24900],[63962,26647],[63963,29575],[63964,38534],[63965,21033],[63966,21519],[63967,23653],[63968,26131],[63969,26446],[63970,26792],[63971,27877],[63972,29702],[63973,30178],[63974,32633],[63975,35023],[63976,35041],[63977,37324],[63978,38626],[63979,21311],[63980,28346],[63981,21533],[63982,29136],[63983,29848],[63984,34298],[63985,38563],[63986,40023],[63987,40607],[63988,26519],[63989,28107],[63990,33256],[63991,31435],[63992,31520],[63993,31890],[63994,29376],[63995,28825],[63996,35672],[63997,20160],[63998,33590],[63999,21050],[64000,20999],[64001,24230],[64002,25299],[64003,31958],[64004,23429],[64005,27934],[64006,26292],[64007,36667],[64008,34892],[64009,38477],[64010,35211],[64011,24275],[64012,20800],[64013,21952],[64016,22618],[64018,26228],[64021,20958],[64022,29482],[64023,30410],[64024,31036],[64025,31070],[64026,31077],[64027,31119],[64028,38742],[64029,31934],[64030,32701],[64032,34322],[64034,35576],[64037,36920],[64038,37117],[64042,39151],[64043,39164],[64044,39208],[64045,40372],[64046,37086],[64047,38583],[64048,20398],[64049,20711],[64050,20813],[64051,21193],[64052,21220],[64053,21329],[64054,21917],[64055,22022],[64056,22120],[64057,22592],[64058,22696],[64059,23652],[64060,23662],[64061,24724],[64062,24936],[64063,24974],[64064,25074],[64065,25935],[64066,26082],[64067,26257],[64068,26757],[64069,28023],[64070,28186],[64071,28450],[64072,29038],[64073,29227],[64074,29730],[64075,30865],[64076,31038],[64077,31049],[64078,31048],[64079,31056],[64080,31062],[64081,31069],[64082,31117],[64083,31118],[64084,31296],[64085,31361],[64086,31680],[64087,32244],[64088,32265],[64089,32321],[64090,32626],[64091,32773],[64092,33261],[64093,33401],[64094,33401],[64095,33879],[64096,35088],[64097,35222],[64098,35585],[64099,35641],[64100,36051],[64101,36104],[64102,36790],[64103,36920],[64104,38627],[64105,38911],[64106,38971],[64107,24693],[64108,148206],[64109,33304],[64112,20006],[64113,20917],[64114,20840],[64115,20352],[64116,20805],[64117,20864],[64118,21191],[64119,21242],[64120,21917],[64121,21845],[64122,21913],[64123,21986],[64124,22618],[64125,22707],[64126,22852],[64127,22868],[64128,23138],[64129,23336],[64130,24274],[64131,24281],[64132,24425],[64133,24493],[64134,24792],[64135,24910],[64136,24840],[64137,24974],[64138,24928],[64139,25074],[64140,25140],[64141,25540],[64142,25628],[64143,25682],[64144,25942],[64145,26228],[64146,26391],[64147,26395],[64148,26454],[64149,27513],[64150,27578],[64151,27969],[64152,28379],[64153,28363],[64154,28450],[64155,28702],[64156,29038],[64157,30631],[64158,29237],[64159,29359],[64160,29482],[64161,29809],[64162,29958],[64163,30011],[64164,30237],[64165,30239],[64166,30410],[64167,30427],[64168,30452],[64169,30538],[64170,30528],[64171,30924],[64172,31409],[64173,31680],[64174,31867],[64175,32091],[64176,32244],[64177,32574],[64178,32773],[64179,33618],[64180,33775],[64181,34681],[64182,35137],[64183,35206],[64184,35222],[64185,35519],[64186,35576],[64187,35531],[64188,35585],[64189,35582],[64190,35565],[64191,35641],[64192,35722],[64193,36104],[64194,36664],[64195,36978],[64196,37273],[64197,37494],[64198,38524],[64199,38627],[64200,38742],[64201,38875],[64202,38911],[64203,38923],[64204,38971],[64205,39698],[64206,40860],[64207,141386],[64208,141380],[64209,144341],[64210,15261],[64211,16408],[64212,16441],[64213,152137],[64214,154832],[64215,163539],[64216,40771],[64217,40846],[64285,1497,1460],[64287,1522,1463],[64298,1513,1473],[64299,1513,1474],[64300,1513,1468,1473],[64301,1513,1468,1474],[64302,1488,1463],[64303,1488,1464],[64304,1488,1468],[64305,1489,1468],[64306,1490,1468],[64307,1491,1468],[64308,1492,1468],[64309,1493,1468],[64310,1494,1468],[64312,1496,1468],[64313,1497,1468],[64314,1498,1468],[64315,1499,1468],[64316,1500,1468],[64318,1502,1468],[64320,1504,1468],[64321,1505,1468],[64323,1507,1468],[64324,1508,1468],[64326,1510,1468],[64327,1511,1468],[64328,1512,1468],[64329,1513,1468],[64330,1514,1468],[64331,1493,1465],[64332,1489,1471],[64333,1499,1471],[64334,1508,1471],[69786,69785,69818],[69788,69787,69818],[69803,69797,69818],[69934,69937,69927],[69935,69938,69927],[70475,70471,70462],[70476,70471,70487],[70843,70841,70842],[70844,70841,70832],[70846,70841,70845],[71098,71096,71087],[71099,71097,71087],[71992,71989,71984],[119134,119127,119141],[119135,119128,119141],[119136,119128,119141,119150],[119137,119128,119141,119151],[119138,119128,119141,119152],[119139,119128,119141,119153],[119140,119128,119141,119154],[119227,119225,119141],[119228,119226,119141],[119229,119225,119141,119150],[119230,119226,119141,119150],[119231,119225,119141,119151],[119232,119226,119141,119151],[194560,20029],[194561,20024],[194562,20033],[194563,131362],[194564,20320],[194565,20398],[194566,20411],[194567,20482],[194568,20602],[194569,20633],[194570,20711],[194571,20687],[194572,13470],[194573,132666],[194574,20813],[194575,20820],[194576,20836],[194577,20855],[194578,132380],[194579,13497],[194580,20839],[194581,20877],[194582,132427],[194583,20887],[194584,20900],[194585,20172],[194586,20908],[194587,20917],[194588,168415],[194589,20981],[194590,20995],[194591,13535],[194592,21051],[194593,21062],[194594,21106],[194595,21111],[194596,13589],[194597,21191],[194598,21193],[194599,21220],[194600,21242],[194601,21253],[194602,21254],[194603,21271],[194604,21321],[194605,21329],[194606,21338],[194607,21363],[194608,21373],[194609,21375],[194610,21375],[194611,21375],[194612,133676],[194613,28784],[194614,21450],[194615,21471],[194616,133987],[194617,21483],[194618,21489],[194619,21510],[194620,21662],[194621,21560],[194622,21576],[194623,21608],[194624,21666],[194625,21750],[194626,21776],[194627,21843],[194628,21859],[194629,21892],[194630,21892],[194631,21913],[194632,21931],[194633,21939],[194634,21954],[194635,22294],[194636,22022],[194637,22295],[194638,22097],[194639,22132],[194640,20999],[194641,22766],[194642,22478],[194643,22516],[194644,22541],[194645,22411],[194646,22578],[194647,22577],[194648,22700],[194649,136420],[194650,22770],[194651,22775],[194652,22790],[194653,22810],[194654,22818],[194655,22882],[194656,136872],[194657,136938],[194658,23020],[194659,23067],[194660,23079],[194661,23000],[194662,23142],[194663,14062],[194664,14076],[194665,23304],[194666,23358],[194667,23358],[194668,137672],[194669,23491],[194670,23512],[194671,23527],[194672,23539],[194673,138008],[194674,23551],[194675,23558],[194676,24403],[194677,23586],[194678,14209],[194679,23648],[194680,23662],[194681,23744],[194682,23693],[194683,138724],[194684,23875],[194685,138726],[194686,23918],[194687,23915],[194688,23932],[194689,24033],[194690,24034],[194691,14383],[194692,24061],[194693,24104],[194694,24125],[194695,24169],[194696,14434],[194697,139651],[194698,14460],[194699,24240],[194700,24243],[194701,24246],[194702,24266],[194703,172946],[194704,24318],[194705,140081],[194706,140081],[194707,33281],[194708,24354],[194709,24354],[194710,14535],[194711,144056],[194712,156122],[194713,24418],[194714,24427],[194715,14563],[194716,24474],[194717,24525],[194718,24535],[194719,24569],[194720,24705],[194721,14650],[194722,14620],[194723,24724],[194724,141012],[194725,24775],[194726,24904],[194727,24908],[194728,24910],[194729,24908],[194730,24954],[194731,24974],[194732,25010],[194733,24996],[194734,25007],[194735,25054],[194736,25074],[194737,25078],[194738,25104],[194739,25115],[194740,25181],[194741,25265],[194742,25300],[194743,25424],[194744,142092],[194745,25405],[194746,25340],[194747,25448],[194748,25475],[194749,25572],[194750,142321],[194751,25634],[194752,25541],[194753,25513],[194754,14894],[194755,25705],[194756,25726],[194757,25757],[194758,25719],[194759,14956],[194760,25935],[194761,25964],[194762,143370],[194763,26083],[194764,26360],[194765,26185],[194766,15129],[194767,26257],[194768,15112],[194769,15076],[194770,20882],[194771,20885],[194772,26368],[194773,26268],[194774,32941],[194775,17369],[194776,26391],[194777,26395],[194778,26401],[194779,26462],[194780,26451],[194781,144323],[194782,15177],[194783,26618],[194784,26501],[194785,26706],[194786,26757],[194787,144493],[194788,26766],[194789,26655],[194790,26900],[194791,15261],[194792,26946],[194793,27043],[194794,27114],[194795,27304],[194796,145059],[194797,27355],[194798,15384],[194799,27425],[194800,145575],[194801,27476],[194802,15438],[194803,27506],[194804,27551],[194805,27578],[194806,27579],[194807,146061],[194808,138507],[194809,146170],[194810,27726],[194811,146620],[194812,27839],[194813,27853],[194814,27751],[194815,27926],[194816,27966],[194817,28023],[194818,27969],[194819,28009],[194820,28024],[194821,28037],[194822,146718],[194823,27956],[194824,28207],[194825,28270],[194826,15667],[194827,28363],[194828,28359],[194829,147153],[194830,28153],[194831,28526],[194832,147294],[194833,147342],[194834,28614],[194835,28729],[194836,28702],[194837,28699],[194838,15766],[194839,28746],[194840,28797],[194841,28791],[194842,28845],[194843,132389],[194844,28997],[194845,148067],[194846,29084],[194847,148395],[194848,29224],[194849,29237],[194850,29264],[194851,149000],[194852,29312],[194853,29333],[194854,149301],[194855,149524],[194856,29562],[194857,29579],[194858,16044],[194859,29605],[194860,16056],[194861,16056],[194862,29767],[194863,29788],[194864,29809],[194865,29829],[194866,29898],[194867,16155],[194868,29988],[194869,150582],[194870,30014],[194871,150674],[194872,30064],[194873,139679],[194874,30224],[194875,151457],[194876,151480],[194877,151620],[194878,16380],[194879,16392],[194880,30452],[194881,151795],[194882,151794],[194883,151833],[194884,151859],[194885,30494],[194886,30495],[194887,30495],[194888,30538],[194889,16441],[194890,30603],[194891,16454],[194892,16534],[194893,152605],[194894,30798],[194895,30860],[194896,30924],[194897,16611],[194898,153126],[194899,31062],[194900,153242],[194901,153285],[194902,31119],[194903,31211],[194904,16687],[194905,31296],[194906,31306],[194907,31311],[194908,153980],[194909,154279],[194910,154279],[194911,31470],[194912,16898],[194913,154539],[194914,31686],[194915,31689],[194916,16935],[194917,154752],[194918,31954],[194919,17056],[194920,31976],[194921,31971],[194922,32000],[194923,155526],[194924,32099],[194925,17153],[194926,32199],[194927,32258],[194928,32325],[194929,17204],[194930,156200],[194931,156231],[194932,17241],[194933,156377],[194934,32634],[194935,156478],[194936,32661],[194937,32762],[194938,32773],[194939,156890],[194940,156963],[194941,32864],[194942,157096],[194943,32880],[194944,144223],[194945,17365],[194946,32946],[194947,33027],[194948,17419],[194949,33086],[194950,23221],[194951,157607],[194952,157621],[194953,144275],[194954,144284],[194955,33281],[194956,33284],[194957,36766],[194958,17515],[194959,33425],[194960,33419],[194961,33437],[194962,21171],[194963,33457],[194964,33459],[194965,33469],[194966,33510],[194967,158524],[194968,33509],[194969,33565],[194970,33635],[194971,33709],[194972,33571],[194973,33725],[194974,33767],[194975,33879],[194976,33619],[194977,33738],[194978,33740],[194979,33756],[194980,158774],[194981,159083],[194982,158933],[194983,17707],[194984,34033],[194985,34035],[194986,34070],[194987,160714],[194988,34148],[194989,159532],[194990,17757],[194991,17761],[194992,159665],[194993,159954],[194994,17771],[194995,34384],[194996,34396],[194997,34407],[194998,34409],[194999,34473],[195000,34440],[195001,34574],[195002,34530],[195003,34681],[195004,34600],[195005,34667],[195006,34694],[195007,17879],[195008,34785],[195009,34817],[195010,17913],[195011,34912],[195012,34915],[195013,161383],[195014,35031],[195015,35038],[195016,17973],[195017,35066],[195018,13499],[195019,161966],[195020,162150],[195021,18110],[195022,18119],[195023,35488],[195024,35565],[195025,35722],[195026,35925],[195027,162984],[195028,36011],[195029,36033],[195030,36123],[195031,36215],[195032,163631],[195033,133124],[195034,36299],[195035,36284],[195036,36336],[195037,133342],[195038,36564],[195039,36664],[195040,165330],[195041,165357],[195042,37012],[195043,37105],[195044,37137],[195045,165678],[195046,37147],[195047,37432],[195048,37591],[195049,37592],[195050,37500],[195051,37881],[195052,37909],[195053,166906],[195054,38283],[195055,18837],[195056,38327],[195057,167287],[195058,18918],[195059,38595],[195060,23986],[195061,38691],[195062,168261],[195063,168474],[195064,19054],[195065,19062],[195066,38880],[195067,168970],[195068,19122],[195069,169110],[195070,38923],[195071,38923],[195072,38953],[195073,169398],[195074,39138],[195075,19251],[195076,39209],[195077,39335],[195078,39362],[195079,39422],[195080,19406],[195081,170800],[195082,39698],[195083,40000],[195084,40189],[195085,19662],[195086,19693],[195087,40295],[195088,172238],[195089,19704],[195090,172293],[195091,172558],[195092,172689],[195093,40635],[195094,19798],[195095,40697],[195096,40702],[195097,40709],[195098,40719],[195099,40726],[195100,40763],[195101,173568]],"composites":[192,193,194,195,196,197,199,200,201,202,203,204,205,206,207,209,210,211,212,213,214,217,218,219,220,221,224,225,226,227,228,229,231,232,233,234,235,236,237,238,239,241,242,243,244,245,246,249,250,251,252,253,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,296,297,298,299,300,301,302,303,304,308,309,310,311,313,314,315,316,317,318,323,324,325,326,327,328,332,333,334,335,336,337,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,416,417,431,432,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,478,479,480,481,482,483,486,487,488,489,490,491,492,493,494,495,496,500,501,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,542,543,550,551,552,553,554,555,556,557,558,559,560,561,562,563,901,902,904,905,906,908,910,911,912,938,939,940,941,942,943,944,970,971,972,973,974,979,980,1024,1025,1027,1031,1036,1037,1038,1049,1081,1104,1105,1107,1111,1116,1117,1118,1142,1143,1217,1218,1232,1233,1234,1235,1238,1239,1242,1243,1244,1245,1246,1247,1250,1251,1252,1253,1254,1255,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1272,1273,1570,1571,1572,1573,1574,1728,1730,1747,2345,2353,2356,2507,2508,2888,2891,2892,2964,3018,3019,3020,3144,3264,3271,3272,3274,3275,3402,3403,3404,3546,3548,3549,3550,4134,6918,6920,6922,6924,6926,6930,6971,6973,6976,6977,6979,7680,7681,7682,7683,7684,7685,7686,7687,7688,7689,7690,7691,7692,7693,7694,7695,7696,7697,7698,7699,7700,7701,7702,7703,7704,7705,7706,7707,7708,7709,7710,7711,7712,7713,7714,7715,7716,7717,7718,7719,7720,7721,7722,7723,7724,7725,7726,7727,7728,7729,7730,7731,7732,7733,7734,7735,7736,7737,7738,7739,7740,7741,7742,7743,7744,7745,7746,7747,7748,7749,7750,7751,7752,7753,7754,7755,7756,7757,7758,7759,7760,7761,7762,7763,7764,7765,7766,7767,7768,7769,7770,7771,7772,7773,7774,7775,7776,7777,7778,7779,7780,7781,7782,7783,7784,7785,7786,7787,7788,7789,7790,7791,7792,7793,7794,7795,7796,7797,7798,7799,7800,7801,7802,7803,7804,7805,7806,7807,7808,7809,7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823,7824,7825,7826,7827,7828,7829,7830,7831,7832,7833,7835,7840,7841,7842,7843,7844,7845,7846,7847,7848,7849,7850,7851,7852,7853,7854,7855,7856,7857,7858,7859,7860,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870,7871,7872,7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886,7887,7888,7889,7890,7891,7892,7893,7894,7895,7896,7897,7898,7899,7900,7901,7902,7903,7904,7905,7906,7907,7908,7909,7910,7911,7912,7913,7914,7915,7916,7917,7918,7919,7920,7921,7922,7923,7924,7925,7926,7927,7928,7929,7936,7937,7938,7939,7940,7941,7942,7943,7944,7945,7946,7947,7948,7949,7950,7951,7952,7953,7954,7955,7956,7957,7960,7961,7962,7963,7964,7965,7968,7969,7970,7971,7972,7973,7974,7975,7976,7977,7978,7979,7980,7981,7982,7983,7984,7985,7986,7987,7988,7989,7990,7991,7992,7993,7994,7995,7996,7997,7998,7999,8000,8001,8002,8003,8004,8005,8008,8009,8010,8011,8012,8013,8016,8017,8018,8019,8020,8021,8022,8023,8025,8027,8029,8031,8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047,8048,8050,8052,8054,8056,8058,8060,8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111,8112,8113,8114,8115,8116,8118,8119,8120,8121,8122,8124,8129,8130,8131,8132,8134,8135,8136,8138,8140,8141,8142,8143,8144,8145,8146,8150,8151,8152,8153,8154,8157,8158,8159,8160,8161,8162,8164,8165,8166,8167,8168,8169,8170,8172,8173,8178,8179,8180,8182,8183,8184,8186,8188,8602,8603,8622,8653,8654,8655,8708,8713,8716,8740,8742,8769,8772,8775,8777,8800,8802,8813,8814,8815,8816,8817,8820,8821,8824,8825,8832,8833,8836,8837,8840,8841,8876,8877,8878,8879,8928,8929,8930,8931,8938,8939,8940,8941,12364,12366,12368,12370,12372,12374,12376,12378,12380,12382,12384,12386,12389,12391,12393,12400,12401,12403,12404,12406,12407,12409,12410,12412,12413,12436,12446,12460,12462,12464,12466,12468,12470,12472,12474,12476,12478,12480,12482,12485,12487,12489,12496,12497,12499,12500,12502,12503,12505,12506,12508,12509,12532,12535,12536,12537,12538,12542,69786,69788,69803,69934,69935,70475,70476,70843,70844,70846,71098,71099,71992]} diff --git a/packages/skill-tree/src/byte-source.ts b/packages/skill-tree/src/byte-source.ts new file mode 100644 index 0000000..008192b --- /dev/null +++ b/packages/skill-tree/src/byte-source.ts @@ -0,0 +1,172 @@ +/** + * Minimal forward-only byte reader over a stream, used by the archive + * readers so content is hashed as it streams and never buffered whole. + */ +import { ERROR_CODES, SkillTreeError } from "./errors"; + +/** Archive bytes: a whole buffer, a web stream, or any async iterable of chunks. */ +export type ByteSource = Uint8Array | ReadableStream | AsyncIterable; + +export function readFailed(detail: string, cause?: unknown): SkillTreeError { + return new SkillTreeError(ERROR_CODES.readFailed, "", detail, cause === undefined ? undefined : { cause }); +} + +async function* iterateStream(stream: ReadableStream): AsyncGenerator { + const reader = stream.getReader(); + try { + for (;;) { + const { done, value } = await reader.read(); + if (done) return; + yield value; + } + } finally { + // Stops an upstream producer (or a decompressor) when reading ends early. + await reader.cancel().catch(() => undefined); + } +} + +export function toAsyncIterable(source: ByteSource): AsyncIterable { + if (source instanceof Uint8Array) { + return (async function* () { + yield source; + })(); + } + if (typeof ReadableStream !== "undefined" && source instanceof ReadableStream) return iterateStream(source); + return source as AsyncIterable; +} + +/** Compressed bytes fed per step. */ +const INFLATE_SLICE = 16 * 1024; +const noop = () => undefined; +const EMPTY = Symbol("empty"); + +/** + * Inflates `source` through a web `DecompressionStream` (gzip or raw deflate), + * pull-driven so a decompression bomb stops near the consumer's limit. A new + * slice is written only when no inflated output is waiting and the previous + * write has been taken by the decompressor; otherwise the writable's queue + * (high-water mark counted in chunks) would accept the whole source. The same + * holds for `pipeThrough`, which pumps input regardless of the consumer. + */ +export async function* decompress(source: AsyncIterable, format: "gzip" | "deflate-raw"): AsyncGenerator { + const stream = new DecompressionStream(format); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + const input = source[Symbol.asyncIterator](); + let pending: Uint8Array = new Uint8Array(0); + let inputDone = false; + // Settles (to EMPTY) once the decompressor has taken the last written slice. + let taken: Promise = Promise.resolve(EMPTY); + let read = reader.read(); + read.catch(noop); + try { + for (;;) { + // Output wins when queued (a read of a queued chunk is already settled); + // otherwise wait until the decompressor took the last slice, then feed. + const ready = inputDone ? await read : await Promise.race([read, taken]); + if (ready !== EMPTY) { + if (ready.done) return; + yield ready.value; + read = reader.read(); + read.catch(noop); + continue; + } + if (pending.length === 0) { + const next = await input.next(); + if (next.done) { + inputDone = true; + writer.close().catch(noop); // a failure surfaces through `read` + continue; + } + pending = next.value; + continue; + } + const slice = pending.subarray(0, INFLATE_SLICE); + pending = pending.subarray(slice.length); + // The DOM typings take only ArrayBuffer-backed views; any Uint8Array works at runtime. + taken = writer.write(slice as Uint8Array).then(() => EMPTY); + taken.catch(noop); // a rejection is rethrown by the race above + } + } finally { + await reader.cancel().catch(noop); + await Promise.resolve(input.return?.()).catch(noop); + } +} + +export class StreamReader { + private readonly it: AsyncIterator; + private chunk: Uint8Array = new Uint8Array(0); + private offset = 0; + private ended = false; + + constructor(source: AsyncIterable) { + this.it = source[Symbol.asyncIterator](); + } + + private async nextChunk(): Promise { + while (!this.ended) { + const next = await this.it.next(); + if (next.done) { + this.ended = true; + return false; + } + if (next.value.length > 0) { + this.chunk = next.value; + this.offset = 0; + return true; + } + } + return false; + } + + /** + * Reads exactly `n` bytes. Returns null when the stream ends before the + * first byte; throws `tree.read_failed` when it ends part way. + */ + async readExact(n: number): Promise { + const out = new Uint8Array(n); + let filled = 0; + while (filled < n) { + if (this.offset >= this.chunk.length && !(await this.nextChunk())) { + if (filled === 0) return null; + throw readFailed(`stream ends after ${filled} of ${n} bytes`); + } + const take = Math.min(n - filled, this.chunk.length - this.offset); + out.set(this.chunk.subarray(this.offset, this.offset + take), filled); + this.offset += take; + filled += take; + } + return out; + } + + /** Yields exactly `n` bytes in chunks; throws `tree.read_failed` if the stream ends first. */ + async *chunks(n: number): AsyncGenerator { + let left = n; + while (left > 0) { + if (this.offset >= this.chunk.length && !(await this.nextChunk())) { + throw readFailed(`stream ends ${left} bytes before the end of an entry`); + } + const take = Math.min(left, this.chunk.length - this.offset); + const piece = this.chunk.subarray(this.offset, this.offset + take); + this.offset += take; + left -= take; + yield piece; + } + } + + async skip(n: number): Promise { + for await (const piece of this.chunks(n)) void piece; + } + + /** Releases the underlying stream after an early stop. */ + async close(): Promise { + if (!this.ended) await this.it.return?.(); + } +} + +/** Wraps any non-contract failure of a reader as `tree.read_failed`. */ +export function asTreeError(err: unknown): SkillTreeError { + if (err instanceof SkillTreeError) return err; + const message = err instanceof Error ? err.message : String(err); + return readFailed(`archive read failed: ${message}`, err); +} diff --git a/packages/skill-tree/src/collision.ts b/packages/skill-tree/src/collision.ts new file mode 100644 index 0000000..395edd4 --- /dev/null +++ b/packages/skill-tree/src/collision.ts @@ -0,0 +1,86 @@ +/** + * File-versus-directory and case-collision checks (contract 4.4, validation + * steps 7 and 8). Keys use Unicode canonical caseless matching, + * NFD(fold(NFD(path))), with the generated Unicode 15.0.0 tables. + */ +import { ERROR_CODES, SkillTreeError, treeError } from "./errors"; +import { fold, nfd } from "./unicode-15"; + +/** Canonical caseless key of a path. Folding never touches "/". */ +export function foldKey(p: string): string { + return nfd(fold(nfd(p))); +} + +/** Every directory prefix of `p`, shortest first. */ +function dirPrefixes(p: string): string[] { + const out: string[] = []; + for (let i = p.indexOf("/"); i >= 0; i = p.indexOf("/", i + 1)) out.push(p.slice(0, i)); + return out; +} + +/** A file whose exact path is also a directory of another file or a directory entry. */ +export function checkFileDirs(files: readonly string[], dirEntries: readonly string[]): SkillTreeError | null { + const dirs = new Map(); + for (const file of files) { + for (const dir of dirPrefixes(file)) if (!dirs.has(dir)) dirs.set(dir, file); + } + for (const file of files) { + const child = dirs.get(file); + if (child !== undefined) { + return treeError(ERROR_CODES.unsafePath, file, `is a file and also the directory of ${JSON.stringify(child)}`); + } + } + const entries = new Set(dirEntries); + for (const file of files) { + if (entries.has(file)) return treeError(ERROR_CODES.unsafePath, file, "is a file and also a directory entry"); + } + return null; +} + +/** + * Paths a case-insensitive file system would merge: two files with equal + * keys (including a path listed twice), a file whose key equals a directory + * or directory entry key, and two distinct directories with equal keys. + * Directory entries are compared with files only. Checks follow input order, + * so a given entry list always reports the same path. + */ +export function checkCollisions(files: readonly string[], dirEntries: readonly string[]): SkillTreeError | null { + const byKey = new Map(); + for (const file of files) { + const key = foldKey(file); + const prior = byKey.get(key); + if (prior !== undefined) { + return treeError(ERROR_CODES.caseCollision, file, `collides with ${JSON.stringify(prior)} under case folding`); + } + byKey.set(key, file); + } + const foldedDirs = new Map(); + let dirConflict: SkillTreeError | null = null; + for (const file of files) { + for (const dir of dirPrefixes(file)) { + const key = foldKey(dir); + const prior = foldedDirs.get(key); + if (prior === undefined) foldedDirs.set(key, { dir, file }); + else if (prior.dir !== dir && dirConflict === null) { + dirConflict = treeError( + ERROR_CODES.caseCollision, + file, + `directory ${JSON.stringify(dir)} collides with directory ${JSON.stringify(prior.dir)} of ${JSON.stringify(prior.file)} under case folding`, + ); + } + } + } + for (const file of files) { + const owner = foldedDirs.get(foldKey(file)); + if (owner !== undefined) { + return treeError(ERROR_CODES.caseCollision, file, `collides under case folding with the directory of ${JSON.stringify(owner.file)}`); + } + } + for (const dir of dirEntries) { + const prior = byKey.get(foldKey(dir)); + if (prior !== undefined) { + return treeError(ERROR_CODES.caseCollision, prior, `collides under case folding with directory entry ${JSON.stringify(dir)}`); + } + } + return dirConflict; +} diff --git a/packages/skill-tree/src/entry-collector.ts b/packages/skill-tree/src/entry-collector.ts new file mode 100644 index 0000000..0951970 --- /dev/null +++ b/packages/skill-tree/src/entry-collector.ts @@ -0,0 +1,71 @@ +/** + * Collects the entries an archive reader presents, enforcing the contract + * limits while content streams (contract 4.6): bytes are counted as they are + * read, header sizes are never trusted, and reading stops at the first limit. + */ +import { ERROR_CODES, treeError } from "./errors"; +import { createSha256 } from "./serialize"; +import type { TreeManifest } from "./serialize"; +import { LIMITS, buildTree } from "./tree"; +import type { TreeEntry, TreeLimits } from "./tree"; + +export class EntryCollector { + readonly entries: TreeEntry[] = []; + private listed = 0; + private files = 0; + private total = 0; + + constructor(private readonly limits: TreeLimits = LIMITS) {} + + /** Counts one presented entry of any kind toward the entry limit. */ + present(): void { + if (++this.listed > this.limits.maxEntries) { + throw treeError(ERROR_CODES.limitExceeded, "", `archive has more than ${this.limits.maxEntries} entries`); + } + } + + /** Hashes one regular file's content as it streams, counting it toward the file and byte limits. */ + async hashFile(path: string, content: AsyncIterable): Promise<{ size: number; sha256: string }> { + if (++this.files > this.limits.maxFiles) { + throw treeError(ERROR_CODES.limitExceeded, path, `tree has more than ${this.limits.maxFiles} files`); + } + const hash = createSha256(); + let size = 0; + for await (const chunk of content) { + size += chunk.length; + this.total += chunk.length; + if (size > this.limits.maxFileSize) { + throw treeError(ERROR_CODES.limitExceeded, path, `file exceeds ${this.limits.maxFileSize} bytes`); + } + if (this.total > this.limits.maxTreeSize) { + throw treeError(ERROR_CODES.limitExceeded, path, `tree exceeds ${this.limits.maxTreeSize} bytes`); + } + hash.update(chunk); + } + return { size, sha256: hash.hex() }; + } + + add(entry: TreeEntry): void { + this.entries.push(entry); + } + + build(): TreeManifest { + return buildTree(this.entries, { listed: this.listed, limits: this.limits }); + } +} + +const utf8Strict = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); +const utf8Lossy = new TextDecoder("utf-8", { ignoreBOM: true }); + +/** + * Decodes raw entry name bytes. Invalid UTF-8 (including WTF-8 surrogates) + * is never repaired: the lossy text only serves noise matching and messages, + * and the entry fails with `tree.unsafe_path`. + */ +export function decodeName(raw: Uint8Array): { path: string; utf8Valid: boolean } { + try { + return { path: utf8Strict.decode(raw), utf8Valid: true }; + } catch { + return { path: utf8Lossy.decode(raw), utf8Valid: false }; + } +} diff --git a/packages/skill-tree/src/errors.ts b/packages/skill-tree/src/errors.ts new file mode 100644 index 0000000..84b2ace --- /dev/null +++ b/packages/skill-tree/src/errors.ts @@ -0,0 +1,42 @@ +/** + * Contract error codes of `agentkit.skill-tree/v1` and package identity + * (AgentKit skill lifecycle contract, section 11). Callers branch on `code`, + * never on the message text. + */ +export const ERROR_CODES = { + identityInvalid: "identity.invalid", + identityReservedPublisher: "identity.reserved_publisher", + identityReservedAlias: "identity.reserved_alias", + unsafePath: "tree.unsafe_path", + nonNfcPath: "tree.non_nfc_path", + caseCollision: "tree.case_collision", + unsupportedEntry: "tree.unsupported_entry", + limitExceeded: "tree.limit_exceeded", + empty: "tree.empty", + readFailed: "tree.read_failed", +} as const; + +export type SkillTreeErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]; + +/** A classified contract failure. `path` names the offending tree path when one exists. */ +export class SkillTreeError extends Error { + readonly code: SkillTreeErrorCode; + readonly path: string; + readonly detail: string; + + constructor(code: SkillTreeErrorCode, path: string, detail: string, options?: { cause?: unknown }) { + super(path === "" ? `${code}: ${detail}` : `${code}: ${JSON.stringify(path)}: ${detail}`, options); + this.name = "SkillTreeError"; + this.code = code; + this.path = path; + this.detail = detail; + } +} + +export function treeError(code: SkillTreeErrorCode, path: string, detail: string): SkillTreeError { + return new SkillTreeError(code, path, detail); +} + +export function isSkillTreeError(value: unknown): value is SkillTreeError { + return value instanceof SkillTreeError; +} diff --git a/packages/skill-tree/src/exposure.ts b/packages/skill-tree/src/exposure.ts new file mode 100644 index 0000000..fc1b003 --- /dev/null +++ b/packages/skill-tree/src/exposure.ts @@ -0,0 +1,76 @@ +/** + * Exposure derivation from SKILL.md frontmatter (AgentKit skill lifecycle + * contract 10.2). Keys are read as YAML 1.2 nodes, never decoded into typed + * booleans: only the plain scalars true/True/TRUE and false/False/FALSE count. + */ +import { isAlias, isMap, isScalar, parseDocument } from "yaml"; +import type { Node, Pair } from "yaml"; + +export type Exposure = "INTERNAL" | "EXPLICIT" | "AUTO"; + +export interface ExposureResult { + exposure: Exposure; + /** True when the frontmatter was unusable and AUTO is a fallback. */ + warning: boolean; +} + +const EXPOSURE_KEYS = new Set(["user-invocable", "disable-model-invocation"]); +const AUTO: ExposureResult = { exposure: "AUTO", warning: false }; +const AUTO_WARN: ExposureResult = { exposure: "AUTO", warning: true }; + +function keyName(key: unknown): string { + if (!isScalar(key)) return ""; + return typeof key.source === "string" ? key.source : String(key.value); +} + +/** An explicit tag, an anchor or an alias makes a key or value unusable. */ +function isUnsafeNode(node: unknown): boolean { + if (isAlias(node)) return true; + if (node === null || typeof node !== "object") return false; + const { anchor, tag } = node as Node; + return (anchor !== undefined && anchor !== "") || (tag !== undefined && tag !== ""); +} + +/** The boolean of a plain scalar spelled exactly as YAML 1.2 core booleans, else undefined. */ +function plainBool(node: unknown): boolean | undefined { + if (!isScalar(node) || node.type !== "PLAIN" || typeof node.source !== "string") return undefined; + if (["true", "True", "TRUE"].includes(node.source)) return true; + if (["false", "False", "FALSE"].includes(node.source)) return false; + return undefined; +} + +/** Exposure of a frontmatter block (the text between the delimiter lines). */ +export function frontmatterExposure(frontmatter: string): ExposureResult { + const doc = parseDocument(frontmatter, { version: "1.2", uniqueKeys: false }); + if (doc.errors.length > 0) return AUTO_WARN; + // An empty or comment-only block holds no document node. + if (doc.contents === null) return AUTO; + if (!isMap(doc.contents)) return AUTO_WARN; + const values = new Map(); + for (const pair of doc.contents.items as Pair[]) { + const name = keyName(pair.key); + if (!EXPOSURE_KEYS.has(name)) continue; + if (values.has(name) || isUnsafeNode(pair.key) || isUnsafeNode(pair.value)) return AUTO_WARN; + values.set(name, pair.value); + } + if (plainBool(values.get("user-invocable")) === false) return { exposure: "INTERNAL", warning: false }; + if (plainBool(values.get("disable-model-invocation")) === true) return { exposure: "EXPLICIT", warning: false }; + return AUTO; +} + +const isFence = (line: string) => line.replace(/[ \t]+$/, "") === "---"; + +/** + * Exposure of a whole SKILL.md: frontmatter is the block between a first + * line "---" and the next "---" line, after removing a UTF-8 BOM and the CR + * of each CRLF. No first fence means no frontmatter; an unclosed block warns. + */ +export function skillFileExposure(file: string): ExposureResult { + const text = (file.startsWith("") ? file.slice(1) : file).replace(/\r\n/g, "\n"); + const lines = text.split("\n"); + if (!isFence(lines[0] ?? "")) return AUTO; + for (let i = 1; i < lines.length; i++) { + if (isFence(lines[i]!)) return frontmatterExposure(lines.slice(1, i).join("\n")); + } + return AUTO_WARN; +} diff --git a/packages/skill-tree/src/identity.ts b/packages/skill-tree/src/identity.ts new file mode 100644 index 0000000..7f28485 --- /dev/null +++ b/packages/skill-tree/src/identity.ts @@ -0,0 +1,153 @@ +/** + * Package identity, provenance trust and alias rules (AgentKit skill + * lifecycle contract section 2). Every check throws a `SkillTreeError` with + * a contract code; nothing here folds case beyond ASCII. + */ +import { ERROR_CODES, treeError } from "./errors"; +import { asciiLower, asciiUpper, segmentViolation } from "./path-rules"; + +/** The SkillX package name pattern; identities compare byte-for-byte after validation. */ +export const IDENTITY_PATTERN = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/; +export const MAX_IDENTITY_SEGMENT_BYTES = 64; +export const MAX_ALIAS_BYTES = 128; + +/** Publisher reserved for content composed from an AgentKit kit. */ +export const RESERVED_PUBLISHER = "agentkit"; + +export const PROVIDERS = ["kit", "local", "git", "skillx"] as const; +export type Provider = (typeof PROVIDERS)[number]; +export const KIT_ORIGINS = ["remote-signed", "local-dev"] as const; +export type KitOrigin = (typeof KIT_ORIGINS)[number]; +export type Trust = "signed-kit" | "publisher-signed" | "local-unverified"; + +const KIT_DIR_PREFIX = "ak-"; + +const invalid = (detail: string) => treeError(ERROR_CODES.identityInvalid, "", detail); + +function splitIdentity(id: string): [publisher: string, slug: string] { + const slash = id.indexOf("/"); + return [id.slice(1, slash), id.slice(slash + 1)]; +} + +/** `@/` whose publisher and slug each pass the tree segment rules and are at most 64 bytes. */ +export function validateIdentity(id: string): void { + if (!IDENTITY_PATTERN.test(id)) throw invalid(`identity ${JSON.stringify(id)} does not match ${IDENTITY_PATTERN.source}`); + for (const segment of splitIdentity(id)) { + // The pattern admits ASCII only, so the length in bytes is the string length. + if (segment.length > MAX_IDENTITY_SEGMENT_BYTES) { + throw invalid(`identity ${JSON.stringify(id)} segment exceeds ${MAX_IDENTITY_SEGMENT_BYTES} bytes`); + } + const reason = segmentViolation(segment); + if (reason !== "") throw invalid(`identity ${JSON.stringify(id)} segment ${JSON.stringify(segment)} ${reason}`); + } +} + +/** Maps a kit skill directory (`ak-plan`) to its identity (`@agentkit/plan`) and portable alias. */ +export function kitIdentity(dirName: string): { identity: string; alias: string } { + if (!dirName.startsWith(KIT_DIR_PREFIX)) throw invalid(`kit skill directory ${JSON.stringify(dirName)} does not start with "ak-"`); + const identity = `@${RESERVED_PUBLISHER}/${dirName.slice(KIT_DIR_PREFIX.length)}`; + validateIdentity(identity); + return { identity, alias: dirName }; +} + +function isProvider(value: string): value is Provider { + return (PROVIDERS as readonly string[]).includes(value); +} + +function checkPublisherFor(identity: string, provider: Provider): void { + validateIdentity(identity); + const [publisher] = splitIdentity(identity); + if (publisher === RESERVED_PUBLISHER && provider !== "kit") { + throw treeError(ERROR_CODES.identityReservedPublisher, "", `provider ${provider} cannot use the reserved publisher of ${identity}`); + } + if (publisher !== RESERVED_PUBLISHER && provider === "kit") { + throw invalid(`kit content must use the "${RESERVED_PUBLISHER}" publisher, not ${identity}`); + } +} + +/** Provider, then identity, then the reserved-publisher pairing, in contract order. */ +export function checkPublisher(identity: string, provider: string): void { + if (!isProvider(provider)) throw invalid(`unknown source provider ${JSON.stringify(provider)} for ${identity}`); + checkPublisherFor(identity, provider); +} + +export interface Provenance { + provider: string; + /** Kit sources only. */ + origin?: string; + /** SkillX releases only: whether publisher provenance verified. */ + verified?: boolean; + /** Whether the composition has a non-empty overlay. */ + overlay?: boolean; +} + +/** Trust of an accepted package; trust follows origin, not identity. */ +export function trustOf(identity: string, p: Provenance): Trust { + if (!isProvider(p.provider)) throw invalid(`unknown source provider ${JSON.stringify(p.provider)} for ${identity}`); + if (p.provider === "kit" && !(KIT_ORIGINS as readonly string[]).includes(p.origin ?? "")) { + throw invalid(`unknown kit origin ${JSON.stringify(p.origin ?? "")} for ${identity}`); + } + checkPublisherFor(identity, p.provider); + if (p.overlay === true) return "local-unverified"; + if (p.provider === "kit" && p.origin === "remote-signed") return "signed-kit"; + if (p.provider === "skillx" && p.verified === true) return "publisher-signed"; + return "local-unverified"; +} + +const hasAlphanumeric = (s: string) => /[A-Za-z0-9]/.test(s); + +function checkAliasShape(alias: string): void { + if (alias === "" || alias.length > MAX_ALIAS_BYTES) throw invalid(`alias ${JSON.stringify(alias)} is not 1 to ${MAX_ALIAS_BYTES} characters`); + if (!/^[A-Za-z0-9._:/$-]+$/.test(alias)) throw invalid(`alias ${JSON.stringify(alias)} contains a character outside [A-Za-z0-9._:/$-]`); + let rest = alias; + if (alias.startsWith("/")) { + rest = alias.slice(1); + if (rest.includes("/") || !rest.includes(":")) { + throw invalid(`alias ${JSON.stringify(alias)} starts with "/" but is not a native namespace:command form`); + } + } + for (const segment of rest.split("/")) { + if (segment === "" || segment === "." || segment === "..") throw invalid(`alias ${JSON.stringify(alias)} has an empty, "." or ".." segment`); + const parts = segment.split(":"); + if (parts.every(hasAlphanumeric)) continue; + if (parts.includes("")) throw invalid(`alias ${JSON.stringify(alias)} has a ":" without text on both sides`); + throw invalid(`alias ${JSON.stringify(alias)} has a segment or ":"-separated part without an ASCII letter or digit`); + } +} + +function checkAliasInput(identity: string, alias: string): void { + validateIdentity(identity); + if (/[^\x00-\x7f]/.test(alias)) throw invalid(`alias ${JSON.stringify(alias)} contains a non-ASCII character`); + checkAliasShape(alias); +} + +function checkReservedAlias(identity: string, alias: string): void { + if (splitIdentity(identity)[0] === RESERVED_PUBLISHER) return; + const segments = asciiUpper(alias.replace(/^\/+/, "")).split("/"); + for (const raw of segments) { + const segment = raw.replace(/^\$+/, ""); + if (segment.startsWith("AK-") || segment.startsWith("AK:")) { + throw treeError(ERROR_CODES.identityReservedAlias, "", `alias ${JSON.stringify(alias)} is reserved for @${RESERVED_PUBLISHER} identities, not ${identity}`); + } + } +} + +/** Validates an invocation alias for an identity: shape first, then the reserved `ak-`/`ak:` prefixes. */ +export function checkAlias(identity: string, alias: string): void { + checkAliasInput(identity, alias); + checkReservedAlias(identity, alias); +} + +/** Validates a projection directory name: one tree-safe segment without ":" or "$". */ +export function checkDirectoryAlias(identity: string, alias: string): void { + checkAliasInput(identity, alias); + if (/[/:$]/.test(alias)) throw invalid(`directory alias ${JSON.stringify(alias)} is not one segment without ":" or "$"`); + const reason = segmentViolation(alias); + if (reason !== "") throw invalid(`directory alias ${JSON.stringify(alias)} ${reason}`); + checkReservedAlias(identity, alias); +} + +/** Group key of a directory alias: only ASCII A-Z mapped to a-z. */ +export function directoryAliasKey(alias: string): string { + return asciiLower(alias); +} diff --git a/packages/skill-tree/src/index.ts b/packages/skill-tree/src/index.ts new file mode 100644 index 0000000..fc01487 --- /dev/null +++ b/packages/skill-tree/src/index.ts @@ -0,0 +1,50 @@ +/** + * `@skillx/skill-tree`: the TypeScript implementation of AgentKit's + * `agentkit.skill-tree/v1` canonical tree digest, its tar and zip readers, + * package identity and alias rules, and frontmatter exposure. It must agree + * with the AgentKit conformance vectors vendored in `fixtures/vectors`. + */ +export { ERROR_CODES, SkillTreeError, isSkillTreeError } from "./errors"; +export type { SkillTreeErrorCode } from "./errors"; + +export { + MODE_EXECUTABLE, + MODE_REGULAR, + MODE_SOURCE_ASSUMED, + MODE_SOURCE_RECORDED, + TREE_SCHEMA, + compareUtf8, + serializeTree, + treeDigest, +} from "./serialize"; +export type { FileMode, ModeSource, TreeFile, TreeManifest } from "./serialize"; + +export { LIMITS, buildTree, headerMode } from "./tree"; +export type { BuildOptions, EntryKind, TreeEntry } from "./tree"; + +export { isNoisePath, validatePath } from "./path-rules"; +export { foldKey } from "./collision"; +export { UNICODE_VERSION } from "./unicode-15"; + +export type { ByteSource } from "./byte-source"; +export { readTarTree, tarEntryKind } from "./tar-reader"; +export type { TarReadOptions } from "./tar-reader"; +export { readZipTree } from "./zip-reader"; +export type { ZipReadOptions } from "./zip-reader"; +export type { RandomAccessSource } from "./zip-records"; + +export { + IDENTITY_PATTERN, + RESERVED_PUBLISHER, + checkAlias, + checkDirectoryAlias, + checkPublisher, + directoryAliasKey, + kitIdentity, + trustOf, + validateIdentity, +} from "./identity"; +export type { KitOrigin, Provenance, Provider, Trust } from "./identity"; + +export { frontmatterExposure, skillFileExposure } from "./exposure"; +export type { Exposure, ExposureResult } from "./exposure"; diff --git a/packages/skill-tree/src/path-rules.ts b/packages/skill-tree/src/path-rules.ts new file mode 100644 index 0000000..624c821 --- /dev/null +++ b/packages/skill-tree/src/path-rules.ts @@ -0,0 +1,155 @@ +/** + * Path and segment rules of `agentkit.skill-tree/v1` (contract 4.4) and the + * excluded noise (contract 4.5). Rules are ordered tables: the first failing + * rule wins, matching the AgentKit reference implementation. + */ +import { ERROR_CODES, SkillTreeError, treeError } from "./errors"; +import { isAssigned, isFormat, isNfc } from "./unicode-15"; + +export const MAX_PATH_BYTES = 512; +export const MAX_SEGMENT_BYTES = 255; +export const MAX_SEGMENTS = 32; + +/** Reserved prefix of AgentKit temporary files; matched exactly and case-sensitively. */ +export const TEMP_PREFIX = ".ak-tmp."; + +const NOISE_DIRS = new Set([".git", ".hg", ".svn", "__pycache__", ".pytest_cache"]); +const NOISE_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]); + +const WINDOWS_RESERVED = new Set([ + "CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$", + ...Array.from({ length: 10 }, (_, i) => `COM${i}`), + ...Array.from({ length: 10 }, (_, i) => `LPT${i}`), + "COM¹", "COM²", "COM³", "LPT¹", "LPT²", "LPT³", +]); + +const MAX_SHORT_NAME_STEM = 8; +const FORBIDDEN_ASCII = new Set(Array.from('\\:<>"|?*')); + +/** UTF-8 byte length of a well-formed string. */ +export function utf8Length(s: string): number { + let n = 0; + for (const ch of s) { + const cp = ch.codePointAt(0)!; + n += cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4; + } + return n; +} + +/** True when the string holds no unpaired surrogate, so it encodes to valid UTF-8. */ +export function isWellFormed(s: string): boolean { + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if (c >= 0xd800 && c <= 0xdbff) { + const next = s.charCodeAt(i + 1); + if (!(next >= 0xdc00 && next <= 0xdfff)) return false; + i++; + } else if (c >= 0xdc00 && c <= 0xdfff) { + return false; + } + } + return true; +} + +export function asciiUpper(s: string): string { + return s.replace(/[a-z]/g, (c) => c.toUpperCase()); +} + +export function asciiLower(s: string): string { + return s.replace(/[A-Z]/g, (c) => c.toLowerCase()); +} + +/** Separators, Windows-forbidden characters, C0/C1 controls, Cf and unassigned (Cn). */ +function forbiddenCodePoint(cp: number): boolean { + if (cp < 0x80 && FORBIDDEN_ASCII.has(String.fromCharCode(cp))) return true; + if (cp < 0x20 || (cp >= 0x7f && cp <= 0x9f)) return true; + if (isFormat(cp)) return true; + return !isAssigned(cp); +} + +function stemOf(segment: string): string { + const dot = segment.indexOf("."); + return dot < 0 ? segment : segment.slice(0, dot); +} + +function isWindowsReservedName(segment: string): boolean { + return WINDOWS_RESERVED.has(asciiUpper(stemOf(segment).replace(/ +$/, ""))); +} + +function isShortNameShaped(segment: string): boolean { + const stem = stemOf(segment); + return utf8Length(stem) <= MAX_SHORT_NAME_STEM && /~[0-9]/.test(stem); +} + +type Rule = readonly [reason: string, bad: (s: string) => boolean]; + +const PATH_RULES: readonly Rule[] = [ + ["is empty", (p) => p === ""], + ["is not valid UTF-8", (p) => !isWellFormed(p)], + [`exceeds ${MAX_PATH_BYTES} bytes`, (p) => utf8Length(p) > MAX_PATH_BYTES], + [`exceeds ${MAX_SEGMENTS} segments`, (p) => p.split("/").length > MAX_SEGMENTS], +]; + +const SEGMENT_RULES: readonly Rule[] = [ + ["is empty (absolute path or repeated separator)", (s) => s === ""], + ["is a relative directory reference", (s) => s === "." || s === ".."], + [`exceeds ${MAX_SEGMENT_BYTES} bytes`, (s) => utf8Length(s) > MAX_SEGMENT_BYTES], + [`starts with the reserved temporary-file prefix ${TEMP_PREFIX}`, (s) => s.startsWith(TEMP_PREFIX)], + [ + "contains a forbidden, control, format or unassigned character", + (s) => Array.from(s).some((ch) => forbiddenCodePoint(ch.codePointAt(0)!)), + ], + ["ends with a space or dot", (s) => s.endsWith(" ") || s.endsWith(".")], + ["is a Windows reserved device name", isWindowsReservedName], + ["is shaped like an 8.3 short name", isShortNameShaped], +]; + +/** The reason of the first segment rule `segment` fails, or "" when it passes. */ +export function segmentViolation(segment: string): string { + for (const [reason, bad] of SEGMENT_RULES) if (bad(segment)) return reason; + return ""; +} + +/** + * Structural path and segment rules, without NFC. `utf8Valid` is false when + * the raw name bytes were not valid UTF-8 and `p` is only a lossy decoding. + */ +export function checkPathRules(p: string, utf8Valid = true): SkillTreeError | null { + if (p === "") return treeError(ERROR_CODES.unsafePath, p, "path is empty"); + if (!utf8Valid) return treeError(ERROR_CODES.unsafePath, p, "path is not valid UTF-8"); + for (const [reason, bad] of PATH_RULES) { + if (bad(p)) return treeError(ERROR_CODES.unsafePath, p, `path ${reason}`); + } + for (const segment of p.split("/")) { + const reason = segmentViolation(segment); + if (reason !== "") return treeError(ERROR_CODES.unsafePath, p, `segment ${JSON.stringify(segment)} ${reason}`); + } + return null; +} + +export function checkNfc(p: string): SkillTreeError | null { + return isNfc(p) ? null : treeError(ERROR_CODES.nonNfcPath, p, "path is not in Unicode NFC"); +} + +/** + * Validates one tree path. Returns null when it is safe, otherwise a + * `tree.unsafe_path` or `tree.non_nfc_path` error. Never throws. + */ +export function validatePath(p: string): SkillTreeError | null { + return checkPathRules(p) ?? checkNfc(p); +} + +export function isNoiseDirName(name: string): boolean { + return NOISE_DIRS.has(name); +} + +export function isNoiseFileName(name: string): boolean { + return NOISE_FILES.has(name); +} + +/** Whether a tree path is noise: a noise directory segment or a noise file base name. */ +export function isNoisePath(p: string): boolean { + const segments = p.split("/"); + const name = segments.pop() ?? ""; + return segments.some(isNoiseDirName) || isNoiseFileName(name); +} diff --git a/packages/skill-tree/src/serialize.ts b/packages/skill-tree/src/serialize.ts new file mode 100644 index 0000000..1230d5f --- /dev/null +++ b/packages/skill-tree/src/serialize.ts @@ -0,0 +1,86 @@ +/** + * Canonical serialization and digest of `agentkit.skill-tree/v1` (contract + * 4.8): a header line, then one ` ` line per file, + * sorted by the raw UTF-8 bytes of the path. The tree digest is `sha256:` + * plus the hex SHA-256 of the whole serialization. + */ +import { createHash } from "node:crypto"; + +export const TREE_SCHEMA = "agentkit.skill-tree/v1"; + +export const MODE_REGULAR = "100644"; +export const MODE_EXECUTABLE = "100755"; +export type FileMode = typeof MODE_REGULAR | typeof MODE_EXECUTABLE; + +export const MODE_SOURCE_RECORDED = "recorded"; +export const MODE_SOURCE_ASSUMED = "assumed"; +export type ModeSource = typeof MODE_SOURCE_RECORDED | typeof MODE_SOURCE_ASSUMED; + +/** One regular file of a tree. `sha256` is the lowercase hex digest of its content. */ +export interface TreeFile { + path: string; + mode: FileMode; + size: number; + sha256: string; +} + +/** The tree manifest; `files` are sorted by raw UTF-8 path bytes. */ +export interface TreeManifest { + schema: typeof TREE_SCHEMA; + digest: string; + modeSource: ModeSource; + files: TreeFile[]; +} + +/** + * Compares two well-formed strings by their UTF-8 bytes, which is code point + * order (UTF-16 code unit order differs above U+FFFF). + */ +export function compareUtf8(a: string, b: string): number { + const ia = a[Symbol.iterator](); + const ib = b[Symbol.iterator](); + for (;;) { + const ca = ia.next(); + const cb = ib.next(); + if (ca.done || cb.done) return ca.done && cb.done ? 0 : ca.done ? -1 : 1; + const diff = ca.value.codePointAt(0)! - cb.value.codePointAt(0)!; + if (diff !== 0) return diff < 0 ? -1 : 1; + } +} + +export function sortFiles(files: readonly T[]): T[] { + return [...files].sort((a, b) => compareUtf8(a.path, b.path)); +} + +/** + * Renders the canonical serialization. Only a validated file set, whose + * paths are unique, has a defined serialization. + */ +export function serializeTree(files: readonly TreeFile[]): string { + let out = `${TREE_SCHEMA}\n`; + for (const f of sortFiles(files)) out += `${f.mode} ${f.sha256} ${f.size} ${f.path}\n`; + return out; +} + +export function sha256Hex(data: string | Uint8Array): string { + return createHash("sha256").update(data).digest("hex"); +} + +/** `sha256:` over the canonical serialization of `files`. */ +export function treeDigest(files: readonly TreeFile[]): string { + return `sha256:${sha256Hex(serializeTree(files))}`; +} + +/** An incremental SHA-256, for hashing file content as it streams. */ +export interface StreamingHash { + update(chunk: Uint8Array): void; + hex(): string; +} + +export function createSha256(): StreamingHash { + const hash = createHash("sha256"); + return { + update: (chunk) => void hash.update(chunk), + hex: () => hash.digest("hex"), + }; +} diff --git a/packages/skill-tree/src/tar-header.ts b/packages/skill-tree/src/tar-header.ts new file mode 100644 index 0000000..86ce14b --- /dev/null +++ b/packages/skill-tree/src/tar-header.ts @@ -0,0 +1,159 @@ +/** + * Tar header and PAX record parsing with the semantics of Go archive/tar, + * the reader the contract names (contract 4.1). Malformed input throws + * `tree.read_failed`. + */ +import { readFailed } from "./byte-source"; + +export const BLOCK = 512; +/** Go archive/tar caps PAX and GNU long-name payloads at 1 MiB. */ +export const MAX_SPECIAL_FILE_SIZE = 1 << 20; + +export interface RawHeader { + /** Raw name bytes (ustar prefix joined), before any PAX or GNU override. */ + name: Uint8Array; + linkname: Uint8Array; + mode: number; + size: number; + typeflag: string; + /** Old GNU sparse ('S') extension flag at offset 482. */ + gnuExtended: boolean; +} + +const latin = new TextDecoder("latin1"); + +function cString(block: Uint8Array, start: number, length: number): Uint8Array { + const field = block.subarray(start, start + length); + const nul = field.indexOf(0); + return nul < 0 ? field : field.subarray(0, nul); +} + +function trimField(field: Uint8Array): string { + return latin.decode(field).replace(/^[ \0]+|[ \0]+$/g, ""); +} + +/** Octal or GNU base-256 numeric field, like Go's parseNumeric. */ +export function parseNumeric(field: Uint8Array, what: string): number { + const first = field[0] ?? 0; + if (first & 0x80) { + if (first & 0x40) throw readFailed(`negative base-256 ${what}`); + let value = first & 0x3f; + for (const byte of field.subarray(1)) { + value = value * 256 + byte; + if (!Number.isSafeInteger(value)) throw readFailed(`${what} overflows`); + } + return value; + } + const text = trimField(field); + if (text === "") return 0; + if (!/^[0-7]+$/.test(text)) throw readFailed(`invalid octal ${what} ${JSON.stringify(text)}`); + const value = parseInt(text, 8); + if (!Number.isSafeInteger(value)) throw readFailed(`${what} overflows`); + return value; +} + +function checksumMatches(block: Uint8Array): boolean { + const stored = trimField(block.subarray(148, 156)); + if (!/^[0-7]+$/.test(stored)) return false; + const want = parseInt(stored, 8); + let unsigned = 0; + let signed = 0; + for (let i = 0; i < BLOCK; i++) { + const byte = i >= 148 && i < 156 ? 0x20 : block[i]!; + unsigned += byte; + signed += byte > 127 ? byte - 256 : byte; + } + return want === unsigned || want === signed; +} + +function startsWith(block: Uint8Array, offset: number, text: string): boolean { + for (let i = 0; i < text.length; i++) if (block[offset + i] !== text.charCodeAt(i)) return false; + return true; +} + +export function isZeroBlock(block: Uint8Array): boolean { + return block.every((b) => b === 0); +} + +export function parseHeader(block: Uint8Array): RawHeader { + if (!checksumMatches(block)) throw readFailed("tar header checksum mismatch"); + let name = cString(block, 0, 100); + const ustar = startsWith(block, 257, "ustar\u000000"); + const gnu = startsWith(block, 257, "ustar \u0000"); + if (ustar) { + // star archives end the header with "tar\0" and keep a shorter prefix. + const star = startsWith(block, 508, "tar\u0000"); + const prefix = cString(block, 345, star ? 131 : 155); + if (prefix.length > 0) { + const joined = new Uint8Array(prefix.length + 1 + name.length); + joined.set(prefix); + joined[prefix.length] = 0x2f; + joined.set(name, prefix.length + 1); + name = joined; + } + } + const typeflag = String.fromCharCode(block[156]!); + return { + name, + linkname: cString(block, 157, 100), + mode: parseNumeric(block.subarray(100, 108), "mode"), + size: parseNumeric(block.subarray(124, 136), "size"), + typeflag, + gnuExtended: gnu && typeflag === "S" && block[482] !== 0, + }; +} + +const utf8 = new TextDecoder("utf-8"); + +export interface PaxRecords { + records: Record; + /** Raw bytes of the `path` record, which may not be valid UTF-8. */ + rawPath?: Uint8Array; +} + +/** Parses PAX records " =\n" like Go's parsePAX; later records win. */ +export function parsePax(data: Uint8Array): PaxRecords { + const records: Record = {}; + let rawPath: Uint8Array | undefined; + let pos = 0; + while (pos < data.length) { + const space = data.indexOf(0x20, pos); + const lenText = space < 0 ? "" : latin.decode(data.subarray(pos, space)); + if (!/^[1-9][0-9]*$/.test(lenText)) throw readFailed("invalid PAX record length"); + const end = pos + Number(lenText); + if (end > data.length || end <= space + 1 || data[end - 1] !== 0x0a) throw readFailed("invalid PAX record"); + const body = data.subarray(space + 1, end - 1); + const eq = body.indexOf(0x3d); + if (eq <= 0) throw readFailed("invalid PAX record key"); + const key = utf8.decode(body.subarray(0, eq)); + const value = utf8.decode(body.subarray(eq + 1)); + const nulInValue = ["path", "linkpath", "uname", "gname"].includes(key) && value.includes("\0"); + if (key.includes("\0") || nulInValue) throw readFailed(`invalid PAX record ${JSON.stringify(key)}`); + records[key] = value; + if (key === "path") rawPath = body.subarray(eq + 1); + pos = end; + } + return rawPath === undefined ? { records } : { records, rawPath }; +} + +const DECIMAL_RECORDS = ["size", "uid", "gid"]; +const TIME_RECORDS = ["atime", "mtime", "ctime"]; + +/** Rejects records Go's mergePAX cannot parse. */ +export function checkPaxValues(records: Readonly>): void { + for (const [key, value] of Object.entries(records)) { + if (DECIMAL_RECORDS.includes(key) && !(/^[0-9]+$/.test(value) && Number.isSafeInteger(Number(value)))) { + throw readFailed(`invalid PAX ${key}`); + } + if (TIME_RECORDS.includes(key) && !/^-?[0-9]+(\.[0-9]+)?$/.test(value)) throw readFailed(`invalid PAX ${key}`); + } +} + +/** Header-only types carry no data regardless of their size field (Go isHeaderOnlyType). */ +export function isHeaderOnly(typeflag: string): boolean { + return ["1", "2", "3", "4", "5", "6"].includes(typeflag); +} + +export function blockPadding(size: number): number { + return (BLOCK - (size % BLOCK)) % BLOCK; +} diff --git a/packages/skill-tree/src/tar-reader.ts b/packages/skill-tree/src/tar-reader.ts new file mode 100644 index 0000000..eca49f6 --- /dev/null +++ b/packages/skill-tree/src/tar-reader.ts @@ -0,0 +1,159 @@ +/** + * Streaming tar (and tar.gz) reader for `agentkit.skill-tree/v1`. An entry is + * a header as Go archive/tar Reader.Next returns it: PAX `x` and GNU `L`/`K` + * headers fold into the following entry, a PAX `g` header is a metadata + * entry. Content is hashed while it streams; nothing is extracted. + */ +import { StreamReader, asTreeError, decompress, readFailed, toAsyncIterable } from "./byte-source"; +import type { ByteSource } from "./byte-source"; +import { EntryCollector, decodeName } from "./entry-collector"; +import type { TreeManifest } from "./serialize"; +import { + BLOCK, + MAX_SPECIAL_FILE_SIZE, + blockPadding, + checkPaxValues, + isHeaderOnly, + isZeroBlock, + parseHeader, + parsePax, +} from "./tar-header"; +import type { PaxRecords } from "./tar-header"; +import { LIMITS, headerMode } from "./tree"; +import type { EntryKind, TreeLimits } from "./tree"; + +export interface TarReadOptions { + /** "auto" (default) detects the gzip magic bytes. */ + compression?: "auto" | "gzip" | "none"; + /** Test hook: smaller limits. */ + limits?: TreeLimits; +} + +/** Maps a tar type flag to an entry kind; type NUL is a directory when its name ends with "/". */ +export function tarEntryKind(typeflag: string, name: string): EntryKind { + switch (typeflag) { + case "0": + return "regular"; + case "\0": + return name.endsWith("/") ? "directory" : "regular"; + case "5": + return "directory"; + case "g": + return "metadata"; + case "2": + return "symlink"; + case "1": + return "hardlink"; + default: + return "special"; + } +} + +async function gunzipIfNeeded(source: AsyncIterable, compression: string): Promise> { + if (compression === "none") return source; + const it = source[Symbol.asyncIterator](); + const head: Uint8Array[] = []; + let seen = 0; + while (seen < 2) { + const next = await it.next(); + if (next.done) break; + head.push(next.value); + seen += next.value.length; + } + const magic = head.flatMap((chunk) => Array.from(chunk.subarray(0, 2))).slice(0, 2); + const gzip = compression === "gzip" || (magic[0] === 0x1f && magic[1] === 0x8b); + const replay = (async function* () { + yield* head; + for (let next = await it.next(); !next.done; next = await it.next()) yield next.value; + })(); + if (!gzip) return replay; + return decompress(replay, "gzip"); +} + +function nulTerminated(data: Uint8Array): Uint8Array { + const nul = data.indexOf(0); + return nul < 0 ? data : data.subarray(0, nul); +} + +async function readSpecial(reader: StreamReader, size: number): Promise { + if (size > MAX_SPECIAL_FILE_SIZE) throw readFailed(`tar extension header of ${size} bytes is too large`); + const data = (await reader.readExact(size)) ?? new Uint8Array(0); + if (data.length !== size) throw readFailed("truncated tar extension header"); + await reader.skip(blockPadding(size)); + return data; +} + +async function readEntries(reader: StreamReader, collector: EntryCollector): Promise { + let pax: PaxRecords | undefined; + let longName: Uint8Array | undefined; + for (;;) { + const block = await reader.readExact(BLOCK); + if (block === null) return; + if (isZeroBlock(block)) { + const second = await reader.readExact(BLOCK); + if (second === null || isZeroBlock(second)) return; + throw readFailed("tar header follows a single zero block"); + } + const h = parseHeader(block); + if (h.typeflag === "x") { + pax = parsePax(await readSpecial(reader, h.size)); + continue; + } + if (h.typeflag === "L" || h.typeflag === "K") { + const value = nulTerminated(await readSpecial(reader, h.size)); + if (h.typeflag === "L" && value.length > 0) longName = value; + continue; + } + collector.present(); + if (h.typeflag === "g") { + // Global records describe the archive; pending x and L headers are dropped, as in Go. + const global = parsePax(await readSpecial(reader, h.size)); + checkPaxValues(global.records); + const name = decodeName(global.rawPath ?? h.name); + collector.add({ path: name.path, utf8Valid: name.utf8Valid, mode: "", size: 0, sha256: "", kind: "metadata", paxRecords: global.records }); + pax = undefined; + longName = undefined; + continue; + } + const records = pax?.records ?? {}; + checkPaxValues(records); + const name = decodeName(longName ?? pax?.rawPath ?? h.name); + const declared = records.size !== undefined ? Number(records.size) : h.size; + const dataSize = isHeaderOnly(h.typeflag) ? 0 : declared; + const kind = tarEntryKind(h.typeflag, name.path); + if (h.gnuExtended) { + // Old GNU sparse headers chain extension blocks before the data. + for (let ext = await reader.readExact(BLOCK); ; ext = await reader.readExact(BLOCK)) { + if (ext === null) throw readFailed("truncated GNU sparse header"); + if (ext[504] === 0) break; + } + } + let size = 0; + let sha256 = ""; + if (kind === "regular") ({ size, sha256 } = await collector.hashFile(name.path, reader.chunks(dataSize))); + else await reader.skip(dataSize); + await reader.skip(blockPadding(dataSize)); + collector.add({ path: name.path, utf8Valid: name.utf8Valid, mode: headerMode(h.mode), size, sha256, kind, paxRecords: records }); + pax = undefined; + longName = undefined; + } +} + +/** + * Reads a tar or tar.gz stream into a validated tree manifest. Throws a + * `SkillTreeError`; malformed archives fail with `tree.read_failed` and + * limits stop reading as soon as they are exceeded. + */ +export async function readTarTree(source: ByteSource, options: TarReadOptions = {}): Promise { + const collector = new EntryCollector(options.limits ?? LIMITS); + let reader: StreamReader | undefined; + try { + reader = new StreamReader(await gunzipIfNeeded(toAsyncIterable(source), options.compression ?? "auto")); + await readEntries(reader, collector); + } catch (err) { + throw asTreeError(err); + } finally { + await reader?.close().catch(() => undefined); + } + return collector.build(); +} diff --git a/packages/skill-tree/src/tree.ts b/packages/skill-tree/src/tree.ts new file mode 100644 index 0000000..961e171 --- /dev/null +++ b/packages/skill-tree/src/tree.ts @@ -0,0 +1,170 @@ +/** + * Validation and build of `agentkit.skill-tree/v1` from the entries an input + * presents (contract 4.1-4.7). Readers hand every entry over, of any kind + * and noise included; `buildTree` applies the validation steps in contract + * order, so every implementation reports the same first code. + */ +import { checkCollisions, checkFileDirs } from "./collision"; +import { ERROR_CODES, SkillTreeError, treeError } from "./errors"; +import { checkNfc, checkPathRules, isNoiseDirName, isNoiseFileName } from "./path-rules"; +import { + MODE_EXECUTABLE, + MODE_REGULAR, + MODE_SOURCE_RECORDED, + TREE_SCHEMA, + sortFiles, + treeDigest, +} from "./serialize"; +import type { FileMode, ModeSource, TreeFile, TreeManifest } from "./serialize"; + +/** Exact contract limits; changing one is a contract version change. */ +export const LIMITS = { + maxEntries: 10_000, + maxFiles: 4_096, + maxFileSize: 32 * 1024 * 1024, + maxTreeSize: 256 * 1024 * 1024, +} as const; +export type TreeLimits = { maxEntries: number; maxFiles: number; maxFileSize: number; maxTreeSize: number }; + +/** + * Entry kinds. `directory` and `metadata` (a PAX global header) add no file + * but count toward the entry limit; every kind except `regular`, `directory` + * and `metadata` fails with `tree.unsupported_entry`. + */ +export type EntryKind = "regular" | "symlink" | "hardlink" | "special" | "directory" | "metadata"; + +/** One presented entry. `mode` is a mode class or the raw octal form of a rejected header mode. */ +export interface TreeEntry { + path: string; + /** False when the raw name bytes were not valid UTF-8 and `path` is a lossy decoding. */ + utf8Valid?: boolean; + mode: string; + size: number; + sha256: string; + kind?: EntryKind; + paxRecords?: Readonly>; + /** Explains why a non-regular or malformed entry is unsupported. */ + reason?: string; +} + +const kindOf = (e: TreeEntry): EntryKind => e.kind ?? "regular"; + +/** Tree path of an entry: a directory entry's one trailing "/" is not part of it. */ +function entryPath(e: TreeEntry): string { + return kindOf(e) === "directory" && e.path.endsWith("/") ? e.path.slice(0, -1) : e.path; +} + +function isNoiseEntry(e: TreeEntry): boolean { + const p = entryPath(e); + const slash = p.lastIndexOf("/"); + const name = p.slice(slash + 1); + if (slash >= 0 && p.slice(0, slash).split("/").some(isNoiseDirName)) return true; + switch (kindOf(e)) { + case "directory": + return isNoiseDirName(name); + case "symlink": + return isNoiseDirName(name) || isNoiseFileName(name); + default: + return isNoiseFileName(name); + } +} + +/** Maps a tar or zip header mode to its class; any other mode keeps its octal form and is rejected. */ +export function headerMode(mode: number): string { + const perm = mode & ~0o170000; + if (perm === 0o644) return MODE_REGULAR; + if (perm === 0o755) return MODE_EXECUTABLE; + return `0${mode.toString(8)}`; +} + +function isSparseRecord(key: string): boolean { + return key.startsWith("GNU.sparse."); +} + +const GLOBAL_ONLY_RECORDS = new Set(["path", "linkpath", "size"]); + +function validateEntry(e: TreeEntry): SkillTreeError | null { + const kind = kindOf(e); + const records = Object.keys(e.paxRecords ?? {}).sort(); + const unsupported = (detail: string) => treeError(ERROR_CODES.unsupportedEntry, e.path, detail); + if (kind === "directory") return null; + if (kind === "metadata") { + const key = records.find((k) => GLOBAL_ONLY_RECORDS.has(k) || isSparseRecord(k)); + return key === undefined ? null : unsupported(`PAX global header carries record ${JSON.stringify(key)}`); + } + if (kind !== "regular") return unsupported(e.reason ?? `entry is not a regular file (${kind})`); + if (e.reason !== undefined) return unsupported(e.reason); + const sparse = records.find(isSparseRecord); + if (sparse !== undefined) return unsupported(`PAX record ${JSON.stringify(sparse)} marks a sparse file`); + if (e.mode !== MODE_REGULAR && e.mode !== MODE_EXECUTABLE) return unsupported(`mode ${JSON.stringify(e.mode)} is not a regular file mode`); + if (!Number.isSafeInteger(e.size) || e.size < 0) return unsupported(`size ${e.size} is not a non-negative integer`); + if (!/^[0-9a-f]{64}$/.test(e.sha256)) return unsupported("sha256 is not 64 lowercase hex characters"); + return null; +} + +/** File count and byte limits over every presented regular-file entry, noise included. */ +export function checkLimits(entries: readonly TreeEntry[], listed: number, limits: TreeLimits): SkillTreeError | null { + if (listed > limits.maxEntries) { + return treeError(ERROR_CODES.limitExceeded, "", `tree has ${listed} entries, more than ${limits.maxEntries}`); + } + let count = 0; + let total = 0; + for (const e of entries) { + if (kindOf(e) !== "regular") continue; + if (++count > limits.maxFiles) return treeError(ERROR_CODES.limitExceeded, e.path, `tree has more than ${limits.maxFiles} files`); + const size = Math.max(e.size, 0); + if (size > limits.maxFileSize) return treeError(ERROR_CODES.limitExceeded, e.path, `file exceeds ${limits.maxFileSize} bytes`); + if ((total += size) > limits.maxTreeSize) return treeError(ERROR_CODES.limitExceeded, e.path, `tree exceeds ${limits.maxTreeSize} bytes`); + } + return null; +} + +function firstError(items: readonly T[], check: (item: T) => SkillTreeError | null): SkillTreeError | null { + for (const item of items) { + const err = check(item); + if (err !== null) return err; + } + return null; +} + +export interface BuildOptions { + /** Total entries the input presented, when it passes on fewer than it listed. */ + listed?: number; + modeSource?: ModeSource; + /** Test hook: smaller limits. Public readers always use the contract limits. */ + limits?: TreeLimits; +} + +/** + * Validates every presented entry and returns the sorted manifest with its + * digest. Throws a `SkillTreeError` with the code of the first failing step. + */ +export function buildTree(entries: readonly TreeEntry[], options: BuildOptions = {}): TreeManifest { + const limits = options.limits ?? LIMITS; + const limitError = checkLimits(entries, options.listed ?? entries.length, limits); + if (limitError !== null) throw limitError; + // A PAX global header's name is metadata: never noise, never path-checked. + const named = entries.filter((e) => kindOf(e) === "metadata" || !isNoiseEntry(e)); + const files = named.filter((e) => kindOf(e) !== "directory" && kindOf(e) !== "metadata"); + const dirs = named.filter((e) => kindOf(e) === "directory").map(entryPath); + const filePaths = files.map((e) => e.path); + const stages: Array<() => SkillTreeError | null> = [ + () => firstError(named, validateEntry), + () => firstError(named, (e) => (kindOf(e) === "metadata" ? null : checkPathRules(entryPath(e), e.utf8Valid ?? true))), + () => firstError(filePaths, checkNfc), + () => (files.length === 0 ? treeError(ERROR_CODES.empty, "", "tree contains no files") : null), + () => checkFileDirs(filePaths, dirs), + () => checkCollisions(filePaths, dirs), + ]; + const failure = firstError(stages, (stage) => stage()); + if (failure !== null) throw failure; + const treeFiles: TreeFile[] = sortFiles( + files.map((e) => ({ path: e.path, mode: e.mode as FileMode, size: e.size, sha256: e.sha256 })), + ); + return { + schema: TREE_SCHEMA, + digest: treeDigest(treeFiles), + modeSource: options.modeSource ?? MODE_SOURCE_RECORDED, + files: treeFiles, + }; +} diff --git a/packages/skill-tree/src/unicode-15-tables.ts b/packages/skill-tree/src/unicode-15-tables.ts new file mode 100644 index 0000000..7936df5 --- /dev/null +++ b/packages/skill-tree/src/unicode-15-tables.ts @@ -0,0 +1,14 @@ +// Code generated by scripts/gen-unicode-tables.mjs. DO NOT EDIT. +// Unicode 15.0.0. Inputs: +// fixtures/ucd/CaseFolding-15.0.0.txt sha256 cdd49e55eae3bbf1f0a3f6580c974a0263cb86a6a08daa10fbf705b4808a56f7 +// scripts/unicode-15.0.0-go.json sha256 baa335155dcbaf0cfe8f40fcc0a98232c4134e821a08c3f198f829a51d911ad5 (go1.25.3, x/text norm 15.0.0) +// Encodings: ranges are "delta.length" base-36 items (delta from the previous +// range end); maps are "keyDelta:value.value"; lists are base-36 deltas. + +export const UNICODE_VERSION = "15.0.0"; +export const ASSIGNED_RANGES = "0.on,3.5,5.6,2.0,2.j,2.b0,2.11,3.1d,3.2,2.1i,9.q,5.5,c.7h,2.1n,3.2s,f.1m,3.1c,3.e,2.r,3.0,2.a,6.u,2.1,7.6j,2.7,3.1,3.l,2.6,2.0,4.3,3.8,3.1,3.3,9.0,5.1,2.4,3.o,3.2,2.5,5.1,3.l,2.6,2.1,2.1,2.1,3.0,2.4,5.1,3.2,4.0,8.3,2.0,8.g,b.2,2.8,2.2,2.l,2.6,2.1,2.4,3.9,2.2,2.2,3.0,g.3,3.b,8.6,2.2,2.7,3.1,3.l,2.6,2.1,2.4,3.8,3.1,3.2,8.2,5.1,2.4,3.h,b.1,2.5,4.2,2.3,4.1,2.0,2.1,4.1,4.2,4.b,5.4,4.2,2.3,3.0,7.0,f.k,6.c,2.2,2.m,2.f,3.8,2.2,2.3,8.1,2.2,3.0,3.3,3.9,8.l,2.2,2.m,2.9,2.4,3.8,2.2,2.3,8.1,7.1,2.3,3.9,2.2,d.c,2.2,2.1e,2.2,2.5,5.f,3.p,2.2,2.h,4.n,2.8,2.0,3.6,4.0,5.5,2.0,2.7,7.9,3.2,d.1l,5.s,12.1,2.0,2.4,2.n,2.0,2.m,3.4,2.0,2.6,2.9,3.3,x.1z,2.z,5.12,2.z,2.e,2.c,12.5h,2.0,6.0,3.ag,2.3,3.6,2.0,2.3,3.14,2.3,3.w,2.3,3.6,2.0,2.3,3.e,2.1k,2.3,3.1u,3.v,4.p,7.2d,3.5,3.ik,4.2g,8.l,a.n,a.j,d.c,2.2,2.1,d.2l,3.9,7.9,7.p,7.2g,8.16,6.1x,b.u,2.b,5.b,5.0,4.15,3.4,c.17,5.p,7.a,4.1p,3.1s,2.s,3.a,7.9,7.d,3.u,1e.24,4.1a,2.37,9.1n,4.e,4.1n,8.16,3.a,9.16,6.et,3.5,3.11,3.5,3.7,2.0,2.0,2.0,2.u,3.1g,2.e,2.d,3.5,2.i,3.2,2.8,2.2s,2.b,3.q,2.c,4.w,g.w,g.3v,5.ie,q.a,m.1eb,3.v,2.9o,6.18,2.0,6.0,3.1j,8.1,f.n,a.6,2.6,2.6,2.6,2.6,2.6,2.6,2.6,2.3h,z.p,2.2g,d.5x,r.b,5.1r,2.2d,3.2u,6.16,2.2l,2.2b,d.1a,2.mlo,4.1i,a.9n,l.53,9.5m,6.1,2.0,2.4,p.1m,4.9,7.1j,9.1x,9.b,7.37,c.t,4.25,2.a,5.w,2.1i,a.d,3.9,3.2u,p.r,b.5,3.5,3.5,a.6,2.6,2.1n,5.3h,3.9,7.8mb,d.m,5.1c,5.6st,3.2x,13.6,d.4,6.p,2.4,2.0,2.1,2.1,2.3g,h.cc,3.1h,8.0,x.15,7.1e,2.i,2.3,5.4,2.3q,3.0,2.59,4.5,3.5,3.5,3.2,4.6,2.6,b.4,3.b,2.p,2.i,2.1,2.e,3.d,z.3e,6.2,5.18,4.2f,2.c,4.0,1c.19,3n.s,4.1c,g.r,5.z,a.t,6.16,6.t,2.10,5.d,17.4d,3.9,7.z,5.z,5.13,9.1f,c.b,2.e,2.6,2.1,2.a,2.e,2.6,2.1,1w.8m,a.l,b.7,p.5,2.15,2.8,1y.5,3.0,2.17,2.1,4.0,3.m,2.1z,9.8,1d.i,2.1,6.w,4.q,6.0,1t.1j,5.j,3.1d,2.1,6.7,2.2,2.s,3.2,5.9,8.8,8.1r,x.12,5.b,a.1h,4.s,3.q,6.p,8.3,d.6,29.20,1k.1e,e.1e,8.19,9.9,87.u,2.15,2.2,3.1,24.16,9.15,n.p,13.r,l.m,a.25,5.z,a.1v,b.0,3.o,8.9,7.1g,2.h,9.12,a.2n,2.j,c.h,2.1a,1r.6,2.0,2.3,2.e,2.a,7.1m,6.9,7.3,2.7,3.1,3.l,2.6,2.1,2.4,2.9,3.1,3.2,3.0,7.0,6.6,3.6,4.4,3w.2j,2.4,v.1z,9.9,4n.1h,3.11,z.1w,c.9,7.c,k.1l,7.9,1j.q,3.e,5.m,56.1n,2t.2a,d.7,3.0,3.7,2.1,2.t,2.1,3.b,a.9,1z.7,3.19,3.a,s.1z,9.2a,e.20,8.9,6v.8,2.18,2.d,b.s,4.v,3.l,2.d,22.6,2.1,2.17,4.0,2.1,2.8,9.9,7.5,2.1,2.10,2.1,2.5,8.9,8n.o,8.g,2.14,4.r,2f.0,g.1d,e.pm,2v.32,2.4,c.5f,219.2q,e.ut,33f.g6,6nu.fs,8.u,2.9,5.28,2.9,7.t,3.5,b.1x,b.9,2.6,2.k,6.i,j5.2i,2u.22,5.1k,8.g,1t.4,c.1,f.4qf,9.yd,17.8,6w8.3,2.6,2.1,2.82,g.0,u.2,3.0,f.3,9.az,1s5.2y,6.c,4.8,8.9,3.7,3ml.19,3.m,a.37,1p.6t,b.12,3.5d,m.1x,3f.j,d.j,d.2e,a.o,3s.2c,2.1y,2.1,3.0,3.1,3.3,2.b,2.0,2.6,2.1s,2.3,3.7,2.6,2.r,2.3,2.4,2.0,4.6,2.9f,3.83,3.jh,g.4,2.e,up.u,7.5,5y.6,2.g,3.6,2.1,2.4,6.1p,y.0,35.18,4.d,3.9,5.1,8x.u,i.1l,6.0,cx.15,kn.6,2.3,2.1,2.e,2.5g,3.f,16.23,5.9,5.1,lu.1v,25.1o,5f.3,2.q,2.1,2.0,3.0,2.9,2.3,2.0,2.0,7.0,5.0,2.0,2.0,2.2,2.1,2.0,3.0,2.0,2.0,2.0,2.0,2.1,2.0,3.3,2.6,2.3,2.3,2.0,2.9,2.g,6.2,2.4,2.g,1h.1,7j.17,5.2r,d.e,3.e,2.e,2.10,b.4t,1l.s,e.17,5.8,8.1,f.5,4b.rb,5.g,4.c,4.3a,5.2m,7.b,5.0,g.b,5.1j,9.9,7.13,9.t,3.1,27.9f,d.d,3.c,4.8,8.19,2.6,9.d,5.8,8.8,8.42,2.1i,12.9,sn.wyn,x.37d,7.65,3.4g1,f.5rk,2e8.f1,15v.3t6,6.38f,f976.0,v.2n,3l.6n,1e6p.1ekd,3.1ekd"; +export const FORMAT_RANGES = "4t.0,11v.5,n.0,5d.0,1e.0,ap.1,29.0,2zw.0,1kt.4,r.4,1e.4,2.9,17yo.0,6y.2,3b6.0,g.0,6zn.f,qxt.3,440.7,h407.0,v.2n"; +export const COMBINING_CLASSES = "lc.k.6e,1.0.6g,1.3.64,1.0.6g,1.0.60,1.4.64,1.1.5m,1.3.64,1.1.5m,1.a.64,1.4.1,1.3.64,1.7.6e,1.0.6o,1.0.6e,1.2.64,1.2.6e,1.1.64,2.2.6e,1.3.64,1.0.6e,1.0.6g,1.1.64,1.0.6e,1.0.6h,1.1.6i,1.0.6h,1.1.6i,1.0.6h,1.c.6e,7o.4.6e,7e.0.64,1.3.6e,1.0.64,1.2.6e,1.0.66,1.0.64,1.5.6e,1.5.64,1.1.6e,1.0.64,1.1.6e,1.0.66,1.0.6c,1.0.6e,1.0.a,1.0.b,1.0.c,1.0.d,1.0.e,1.0.f,1.0.g,1.0.h,1.0.i,1.1.j,1.0.k,1.0.l,1.0.m,2.0.n,2.0.o,1.0.p,2.0.6e,1.0.64,2.0.i,21.7.6e,1.0.u,1.0.v,1.0.w,1d.0.r,1.0.s,1.0.t,1.0.u,1.0.v,1.0.w,1.0.x,1.0.y,1.1.6e,1.1.64,1.4.6e,1.0.64,1.1.6e,1.0.64,h.0.z,2u.6.6e,3.3.6e,1.0.64,1.0.6e,3.1.6e,2.0.64,1.1.6e,1.0.64,10.0.10,v.0.6e,1.0.64,1.1.6e,1.0.64,1.1.6e,1.2.64,1.0.6e,1.1.64,1.0.6e,1.0.64,1.2.6e,1.0.64,1.0.6e,1.0.64,1.0.6e,1.0.64,1.0.6e,1.0.64,1.1.6e,4h.6.6e,1.0.64,1.0.6e,a.0.64,p.3.6e,2.8.6e,2.2.6e,2.4.6e,18.2.64,1p.0.6e,1.2.64,1.3.6e,17.4.6e,1.4.64,1.d.6e,2.0.64,1.1.6e,1.0.64,1.1.6e,1.0.64,1.2.6e,1.2.64,1.0.r,1.0.s,1.0.t,1.2.6e,1.0.64,1.1.6e,1.1.64,1.4.6e,1p.0.7,h.0.9,4.0.6e,1.0.64,1.1.6e,2w.0.7,h.0.9,1d.0.6e,1q.0.7,h.0.9,33.0.7,h.0.9,33.0.7,h.0.9,3k.0.9,33.0.7,h.0.9,8.0.2c,1.0.2j,2u.0.7,h.0.9,32.1.9,h.0.9,3h.0.9,32.1.2v,1.0.9,e.3.2z,31.1.3a,1.0.9,e.3.3e,25.1.64,s.0.64,2.0.64,2.0.60,1k.0.3l,1.0.3m,2.0.3o,6.3.3m,3.0.3m,2.1.6e,1.0.9,2.1.6e,1r.0.64,35.0.7,2.1.9,2b.0.64,k0.2.6e,qd.1.9,v.0.9,4e.0.9,b.0.6e,5o.0.6c,40.0.66,1.0.6e,1.0.64,64.0.6e,1.0.64,20.0.9,l.7.6e,3.0.64,1d.4.6e,1.5.64,1.1.6e,1.0.64,2.1.64,1.1.6e,1.1.64,1.4.6e,1.0.64,1.3.6e,2u.0.7,g.0.9,13.0.6e,1.0.64,1.6.6e,1j.1.9,1n.0.7,c.1.9,1w.0.7,49.2.6e,2.0.1,1.4.64,1.1.6e,1.3.64,1.0.6e,2.6.1,5.0.64,7.0.6e,4.1.6e,5j.1.6e,1.0.64,1.6.6e,1.0.64,1.1.6e,1.0.6i,1.0.5y,1.0.64,1.0.5m,1.10.6e,1.0.6g,1.1.6c,1.0.64,1.0.62,1.0.6e,1.0.6h,1.0.64,1.0.6e,1.0.64,k1.1.6e,1.1.1,1.3.6e,1.2.1,1.1.6e,5.0.6e,4.1.1,1.0.6e,1.0.64,1.0.6e,1.1.1,1.3.64,1.0.6e,2db.2.6e,3y.0.9,2p.v.6e,ff.0.62,1.0.6c,1.0.6g,1.0.66,1.1.68,2y.1.8,n9x.0.6e,5.9.6e,x.1.6e,29.1.6e,7p.0.9,12.0.9,48.0.9,s.h.6e,1m.2.64,12.0.9,2o.0.7,d.0.9,6o.0.6e,2.1.6e,1.0.64,3.1.6e,6.1.6e,2.0.6e,1h.0.9,6v.0.9,fn5.0.q,le.6.6e,1.6.64,1.1.6e,r2.0.64,6b.0.64,46.4.6e,1ar.0.64,2.0.6e,15.0.6e,1.0.1,1.0.64,5.0.9,4m.0.6e,1.0.64,fy.3.6e,as.1.6e,29.2.64,1z.1.64,1.2.6e,1.0.64,1.0.6e,1.3.64,1e.0.6e,1.0.64,1.0.6e,1.0.64,5d.0.9,16.0.9,f.0.9,1m.0.9,1.0.7,1y.2.6e,1d.1.9,1r.0.7,25.0.9,a.0.7,2z.0.9,1.0.7,4z.0.7,1.0.9,29.1.7,h.0.9,p.6.6e,4.4.6e,5q.0.9,4.0.7,o.0.6e,2s.0.9,1.0.7,70.0.9,1.0.7,3j.0.9,3b.0.9,1.0.7,38.0.9,7i.0.9,1.0.7,77.1.9,5.0.7,4d.0.9,2c.0.9,j.0.9,2a.0.9,bq.0.9,77.0.7,2.1.9,2a.0.9,bu.1.9,ey6.4.1,1o.6.6e,xm.1.6,f59.0.1,43r.1.60,1.2.1,4.0.6a,1.4.60,9.7.64,3.4.6e,1.1.64,v.3.6e,45.2.6e,2po.6.6e,2.g.6e,3.6.6e,2.1.6e,2.4.6e,2t.0.6e,4h.6.6e,ag.0.6e,1q.3.6e,e5.1.6g,1.0.64,1.0.6e,rl.6.64,32.5.6e,1.0.7"; +export const CANONICAL_DECOMPOSITIONS = "5c:1t.lc,1:1t.ld,1:1t.le,1:1t.lf,1:1t.lk,1:1t.lm,2:1v.mf,1:1x.lc,1:1x.ld,1:1x.le,1:1x.lk,1:21.lc,1:21.ld,1:21.le,1:21.lk,2:26.lf,1:27.lc,1:27.ld,1:27.le,1:27.lf,1:27.lk,3:2d.lc,1:2d.ld,1:2d.le,1:2d.lk,1:2h.ld,3:2p.lc,1:2p.ld,1:2p.le,1:2p.lf,1:2p.lk,1:2p.lm,2:2r.mf,1:2t.lc,1:2t.ld,1:2t.le,1:2t.lk,1:2x.lc,1:2x.ld,1:2x.le,1:2x.lk,2:32.lf,1:33.lc,1:33.ld,1:33.le,1:33.lf,1:33.lk,3:39.lc,1:39.ld,1:39.le,1:39.lk,1:3d.ld,2:3d.lk,1:1t.lg,1:2p.lg,1:1t.li,1:2p.li,1:1t.mg,1:2p.mg,1:1v.ld,1:2r.ld,1:1v.le,1:2r.le,1:1v.lj,1:2r.lj,1:1v.lo,1:2r.lo,1:1w.lo,1:2s.lo,3:1x.lg,1:2t.lg,1:1x.li,1:2t.li,1:1x.lj,1:2t.lj,1:1x.mg,1:2t.mg,1:1x.lo,1:2t.lo,1:1z.le,1:2v.le,1:1z.li,1:2v.li,1:1z.lj,1:2v.lj,1:1z.mf,1:2v.mf,1:20.le,1:2w.le,3:21.lf,1:2x.lf,1:21.lg,1:2x.lg,1:21.li,1:2x.li,1:21.mg,1:2x.mg,1:21.lj,4:22.le,1:2y.le,1:23.mf,1:2z.mf,2:24.ld,1:30.ld,1:24.mf,1:30.mf,1:24.lo,1:30.lo,5:26.ld,1:32.ld,1:26.mf,1:32.mf,1:26.lo,1:32.lo,4:27.lg,1:33.lg,1:27.li,1:33.li,1:27.ln,1:33.ln,3:2a.ld,1:36.ld,1:2a.mf,1:36.mf,1:2a.lo,1:36.lo,1:2b.ld,1:37.ld,1:2b.le,1:37.le,1:2b.mf,1:37.mf,1:2b.lo,1:37.lo,1:2c.mf,1:38.mf,1:2c.lo,1:38.lo,3:2d.lf,1:39.lf,1:2d.lg,1:39.lg,1:2d.li,1:39.li,1:2d.lm,1:39.lm,1:2d.ln,1:39.ln,1:2d.mg,1:39.mg,1:2f.le,1:3b.le,1:2h.le,1:3d.le,1:2h.lk,1:2i.ld,1:3e.ld,1:2i.lj,1:3e.lj,1:2i.lo,1:3e.lo,y:27.m3,1:33.m3,e:2d.m3,1:39.m3,t:1t.lo,1:2p.lo,1:21.lo,1:2x.lo,1:27.lo,1:33.lo,1:2d.lo,1:39.lo,1:64.lg,1:70.lg,1:64.ld,1:70.ld,1:64.lo,1:70.lo,1:64.lc,1:70.lc,2:5g.lg,1:6c.lg,1:fa.lg,1:fb.lg,1:5i.lg,1:6e.lg,3:1z.lo,1:2v.lo,1:23.lo,1:2z.lo,1:27.mg,1:33.mg,1:dm.lg,1:dn.lg,1:c7.lo,1:ia.lo,1:2y.lo,4:1z.ld,1:2v.ld,3:26.lc,1:32.lc,1:5h.ld,1:6d.ld,1:5i.ld,1:6e.ld,1:60.ld,1:6w.ld,1:1t.lr,1:2p.lr,1:1t.lt,1:2p.lt,1:1x.lr,1:2t.lr,1:1x.lt,1:2t.lt,1:21.lr,1:2x.lr,1:21.lt,1:2x.lt,1:27.lr,1:33.lr,1:27.lt,1:33.lt,1:2a.lr,1:36.lr,1:2a.lt,1:36.lt,1:2d.lr,1:39.lr,1:2d.lt,1:39.lt,1:2b.me,1:37.me,1:2c.me,1:38.me,3:20.lo,1:2w.lo,7:1t.lj,1:2p.lj,1:1x.mf,1:2t.mf,1:5y.lg,1:6u.lg,1:5x.lg,1:6t.lg,1:27.lj,1:33.lj,1:fi.lg,1:fj.lg,1:2h.lg,1:3d.lg,7h:lc,1:ld,2:lv,1:lk.ld,1c:jd,a:1n,7:4o.ld,1:pd.ld,1:53,1:ph.ld,1:pj.ld,1:pl.ld,2:pr.ld,2:px.ld,1:q1.ld,1:qy.ld,q:pl.lk,1:px.lk,1:q9.ld,1:qd.ld,1:qf.ld,1:qh.ld,1:qz.ld,q:qh.lk,1:qt.lk,1:qn.ld,1:qt.ld,1:qx.ld,5:r6.ld,1:r6.lk,18:t1.lc,1:t1.lk,2:sz.ld,4:sm.lk,5:t6.ld,1:t4.lc,1:tf.li,b:t4.li,w:u0.li,n:tx.lc,1:tx.lk,2:tv.ld,4:uu.lk,5:u2.ld,1:u0.lc,1:ub.li,o:vo.lr,1:vp.lr,22:t2.li,1:ty.li,e:sw.li,1:ts.li,1:sw.lk,1:ts.lk,3:t1.li,1:tx.li,3:yg.lk,1:yh.lk,1:t2.lk,1:ty.lk,1:t3.lk,1:tz.lk,3:t4.lg,1:u0.lg,1:t4.lk,1:u0.lk,1:ta.lk,1:u6.lk,3:yw.lk,1:yx.lk,1:tp.lk,1:ul.lk,1:tf.lg,1:ub.lg,1:tf.lk,1:ub.lk,1:tf.ln,1:ub.ln,1:tj.lk,1:uf.lk,3:tn.lk,1:uj.lk,89:17r.18z,1:17r.190,1:18o.190,1:17r.191,1:18q.190,4a:1cl.190,2:1c1.190,h:1ci.190,gm:1t4.1to,8:1tc.1to,3:1tf.1to,10:1sl.1to,1:1sm.1to,1:1sn.1to,1:1ss.1to,1:1sx.1to,1:1sy.1to,1:1t7.1to,1:1tb.1to,30:1xj.1xa,1:1xj.1xz,g:1wh.1x8,1:1wi.1x8,2:1wv.1x8,2c:20i.20s,3:20o.20s,z:1zq.20s,1:1zr.20s,1:1zw.20s,3:20b.20s,6i:287.28m,3:287.27y,1:287.28n,g:275.27w,1:276.27w,1j:2aa.2c7,1i:2bq.2bi,1:2br.2bi,1:2bq.2c7,3g:2fa.2fq,3c:2in.2j9,7:2iu.2j9,1:2iu.2ja,2:2iu.2iq,1:2iy.2j9,3j:2me.2m6,1:2mf.2m6,1:2me.2mv,3y:2qh.2q2,2:2qh.2q7,1:2qk.2q2,1:2qh.2qn,9x:30i.33r,a:30s.33r,5:30x.33r,5:312.33r,5:317.33r,d:30g.33p,a:31t.31u,2:31t.31w,1:33m.328,2:33n.328,9:31t.328,i:32q.33r,a:330.33r,5:335.33r,5:33a.33r,5:33f.33r,d:32o.33p,31:36t.372,25c:5c5.5dh,2:5c7.5dh,2:5c9.5dh,2:5cb.5dh,2:5cd.5dh,4:5ch.5dh,15:5dm.5dh,2:5do.5dh,3:5dq.5dh,1:5dr.5dh,2:5du.5dh,jh:1t.md,1:2p.md,1:1u.lj,1:2q.lj,1:1u.mb,1:2q.mb,1:1u.mp,1:2q.mp,1:5j.ld,1:6f.ld,1:1w.lj,1:2s.lj,1:1w.mb,1:2s.mb,1:1w.mp,1:2s.mp,1:1w.mf,1:2s.mf,1:1w.ml,1:2s.ml,1:7m.lc,1:7n.lc,1:7m.ld,1:7n.ld,1:1x.ml,1:2t.ml,1:1x.mo,1:2t.mo,1:fc.li,1:fd.li,1:1y.lj,1:2u.lj,1:1z.lg,1:2v.lg,1:20.lj,1:2w.lj,1:20.mb,1:2w.mb,1:20.lk,1:2w.lk,1:20.mf,1:2w.mf,1:20.mm,1:2w.mm,1:21.mo,1:2x.mo,1:5r.ld,1:6n.ld,1:23.ld,1:2z.ld,1:23.mb,1:2z.mb,1:23.mp,1:2z.mp,1:24.mb,1:30.mb,1:5yu.lg,1:5yv.lg,1:24.mp,1:30.mp,1:24.ml,1:30.ml,1:25.ld,1:31.ld,1:25.lj,1:31.lj,1:25.mb,1:31.mb,1:26.lj,1:32.lj,1:26.mb,1:32.mb,1:26.mp,1:32.mp,1:26.ml,1:32.ml,1:5x.ld,1:6t.ld,1:5x.lk,1:6t.lk,1:98.lc,1:99.lc,1:98.ld,1:99.ld,1:28.ld,1:34.ld,1:28.lj,1:34.lj,1:2a.lj,1:36.lj,1:2a.mb,1:36.mb,1:5zu.lg,1:5zv.lg,1:2a.mp,1:36.mp,1:2b.lj,1:37.lj,1:2b.mb,1:37.mb,1:9m.lj,1:9n.lj,1:9s.lj,1:9t.lj,1:602.lj,1:603.lj,1:2c.lj,1:38.lj,1:2c.mb,1:38.mb,1:2c.mp,1:38.mp,1:2c.ml,1:38.ml,1:2d.mc,1:39.mc,1:2d.mo,1:39.mo,1:2d.ml,1:39.ml,1:a0.ld,1:a1.ld,1:a2.lk,1:a3.lk,1:2e.lf,1:3a.lf,1:2e.mb,1:3a.mb,1:2f.lc,1:3b.lc,1:2f.ld,1:3b.ld,1:2f.lk,1:3b.lk,1:2f.lj,1:3b.lj,1:2f.mb,1:3b.mb,1:2g.lj,1:3c.lj,1:2g.lk,1:3c.lk,1:2h.lj,1:3d.lj,1:2i.le,1:3e.le,1:2i.mb,1:3e.mb,1:2i.mp,1:3e.mp,1:2w.mp,1:38.lk,1:3b.lm,1:3d.lm,2:an.lj,5:1t.mb,1:2p.mb,1:1t.ll,1:2p.ll,1:5e.ld,1:6a.ld,1:5e.lc,1:6a.lc,1:5e.ll,1:6a.ll,1:5e.lf,1:6a.lf,1:61s.le,1:61t.le,1:76.ld,1:77.ld,1:76.lc,1:77.lc,1:76.ll,1:77.ll,1:76.lf,1:77.lf,1:61s.li,1:61t.li,1:1x.mb,1:2t.mb,1:1x.ll,1:2t.ll,1:1x.lf,1:2t.lf,1:5m.ld,1:6i.ld,1:5m.lc,1:6i.lc,1:5m.ll,1:6i.ll,1:5m.lf,1:6i.lf,1:62g.le,1:62h.le,1:21.ll,1:2x.ll,1:21.mb,1:2x.mb,1:27.mb,1:33.mb,1:27.ll,1:33.ll,1:5w.ld,1:6s.ld,1:5w.lc,1:6s.lc,1:5w.ll,1:6s.ll,1:5w.lf,1:6s.lf,1:630.le,1:631.le,1:bk.ld,1:bl.ld,1:bk.lc,1:bl.lc,1:bk.ll,1:bl.ll,1:bk.lf,1:bl.lf,1:bk.mb,1:bl.mb,1:2d.mb,1:39.mb,1:2d.ll,1:39.ll,1:bz.ld,1:c0.ld,1:bz.lc,1:c0.lc,1:bz.ll,1:c0.ll,1:bz.lf,1:c0.lf,1:bz.mb,1:c0.mb,1:2h.lc,1:3d.lc,1:2h.mb,1:3d.mb,1:2h.ll,1:3d.ll,1:2h.lf,1:3d.lf,7:q9.lv,1:q9.lw,1:64g.lc,1:64h.lc,1:64g.ld,1:64h.ld,1:64g.n6,1:64h.n6,1:pd.lv,1:pd.lw,1:64o.lc,1:64p.lc,1:64o.ld,1:64p.ld,1:64o.n6,1:64p.n6,1:qd.lv,1:qd.lw,1:64w.lc,1:64x.lc,1:64w.ld,1:64x.ld,3:ph.lv,1:ph.lw,1:654.lc,1:655.lc,1:654.ld,1:655.ld,3:qf.lv,1:qf.lw,1:65c.lc,1:65d.lc,1:65c.ld,1:65d.ld,1:65c.n6,1:65d.n6,1:pj.lv,1:pj.lw,1:65k.lc,1:65l.lc,1:65k.ld,1:65l.ld,1:65k.n6,1:65l.n6,1:qh.lv,1:qh.lw,1:65s.lc,1:65t.lc,1:65s.ld,1:65t.ld,1:65s.n6,1:65t.n6,1:pl.lv,1:pl.lw,1:660.lc,1:661.lc,1:660.ld,1:661.ld,1:660.n6,1:661.n6,1:qn.lv,1:qn.lw,1:668.lc,1:669.lc,1:668.ld,1:669.ld,3:pr.lv,1:pr.lw,1:66g.lc,1:66h.lc,1:66g.ld,1:66h.ld,3:qt.lv,1:qt.lw,1:66o.lc,1:66p.lc,1:66o.ld,1:66p.ld,1:66o.n6,1:66p.n6,2:px.lw,2:66x.lc,2:66x.ld,2:66x.n6,1:qx.lv,1:qx.lw,1:674.lc,1:675.lc,1:674.ld,1:675.ld,1:674.n6,1:675.n6,1:q1.lv,1:q1.lw,1:67c.lc,1:67d.lc,1:67c.ld,1:67d.ld,1:67c.n6,1:67d.n6,1:q9.lc,1:q9.ld,1:qd.lc,1:qd.ld,1:qf.lc,1:qf.ld,1:qh.lc,1:qh.ld,1:qn.lc,1:qn.ld,1:qt.lc,1:qt.ld,1:qx.lc,1:qx.ld,3:64g.n9,1:64h.n9,1:64i.n9,1:64j.n9,1:64k.n9,1:64l.n9,1:64m.n9,1:64n.n9,1:64o.n9,1:64p.n9,1:64q.n9,1:64r.n9,1:64s.n9,1:64t.n9,1:64u.n9,1:64v.n9,1:65c.n9,1:65d.n9,1:65e.n9,1:65f.n9,1:65g.n9,1:65h.n9,1:65i.n9,1:65j.n9,1:65k.n9,1:65l.n9,1:65m.n9,1:65n.n9,1:65o.n9,1:65p.n9,1:65q.n9,1:65r.n9,1:674.n9,1:675.n9,1:676.n9,1:677.n9,1:678.n9,1:679.n9,1:67a.n9,1:67b.n9,1:67c.n9,1:67d.n9,1:67e.n9,1:67f.n9,1:67g.n9,1:67h.n9,1:67i.n9,1:67j.n9,1:q9.li,1:q9.lg,1:67k.n9,1:q9.n9,1:q4.n9,2:q9.n6,1:69i.n9,1:pd.li,1:pd.lg,1:pd.lc,1:pd.ld,1:pd.n9,2:qh,3:4o.n6,1:67o.n9,1:qf.n9,1:q6.n9,2:qf.n6,1:69y.n9,1:ph.lc,1:ph.ld,1:pj.lc,1:pj.ld,1:pj.n9,1:69r.lc,1:69r.ld,1:69r.n6,1:qh.li,1:qh.lg,1:qy.lc,1:qh.lk.ld,3:qh.n6,1:qy.n6,1:pl.li,1:pl.lg,1:pl.lc,1:pl.ld,2:6bi.lc,1:6bi.ld,1:6bi.n6,1:qt.li,1:qt.lg,1:qz.lc,1:qt.lk.ld,1:qp.lv,1:qp.lw,1:qt.n6,1:qz.n6,1:px.li,1:px.lg,1:px.lc,1:px.ld,1:pt.lw,1:4o.lc,1:4o.ld,1:2o,3:67w.n9,1:qx.n9,1:r2.n9,2:qx.n6,1:6ba.n9,1:pr.lc,1:pr.ld,1:q1.lc,1:q1.ld,1:q1.n9,1:50,3:6bm,1:6bn,85:q1,4:23,1:1t.lm,33:6mo.mw,1:6mq.mw,j:6ms.mw,v:6og.mw,1:6ok.mw,1:6oi.mw,1h:6pv.mw,5:6q0.mw,3:6q3.mw,o:6qr.mw,2:6qt.mw,r:6rg.mw,3:6rn.mw,3:6rp.mw,2:6rs.mw,n:1p.mw,2:6sh.mw,b:6rx.mw,1:1o.mw,1:1q.mw,1:6sk.mw,1:6sl.mw,3:6sy.mw,1:6sz.mw,3:6t2.mw,1:6t3.mw,7:6t6.mw,1:6t7.mw,3:6te.mw,1:6tf.mw,3:6ti.mw,1:6tj.mw,z:6ua.mw,1:6ug.mw,1:6uh.mw,1:6uj.mw,1d:6t8.mw,1:6t9.mw,1:6tt.mw,1:6tu.mw,7:6uq.mw,1:6ur.mw,1:6us.mw,1:6ut.mw,1o:9hk,1:9hl,1iq:8gt.mw,12o:9jf.9ll,2:9jh.9ll,2:9jj.9ll,2:9jl.9ll,2:9jn.9ll,2:9jp.9ll,2:9jr.9ll,2:9jt.9ll,2:9jv.9ll,2:9jx.9ll,2:9jz.9ll,2:9k1.9ll,3:9k4.9ll,2:9k6.9ll,2:9k8.9ll,7:9kf.9ll,1:9kf.9lm,2:9ki.9ll,1:9ki.9lm,2:9kl.9ll,1:9kl.9lm,2:9ko.9ll,1:9ko.9lm,2:9kr.9ll,1:9kr.9lm,n:9ja.9ll,a:9lp.9ll,e:9m3.9ll,2:9m5.9ll,2:9m7.9ll,2:9m9.9ll,2:9mb.9ll,2:9md.9ll,2:9mf.9ll,2:9mh.9ll,2:9mj.9ll,2:9ml.9ll,2:9mn.9ll,2:9mp.9ll,3:9ms.9ll,2:9mu.9ll,2:9mw.9ll,7:9n3.9ll,1:9n3.9lm,2:9n6.9ll,1:9n6.9lm,2:9n9.9ll,1:9n9.9lm,2:9nc.9ll,1:9nc.9lm,2:9nf.9ll,1:9nf.9lm,n:9ly.9ll,3:9nz.9ll,1:9o0.9ll,1:9o1.9ll,1:9o2.9ll,4:9od.9ll,13ia:rpk,1:kc4,1:s7e,1:rt4,1:lw1,1:fg2,1:gkl,1:vj0,1:vj0,1:hn5,1:ssx,1:gw7,1:hmw,1:jcm,1:ne1,1:p6t,1:qin,1:qt6,1:r20,1:sjz,1:kxu,1:ljv,1:mah,1:mvy,1:q4d,1:sq2,1:uep,1:fia,1:ghh,1:l50,1:mjf,1:qi5,1:v8e,1:ifk,1:m3v,1:qfh,1:r50,1:jih,1:pnc,1:qvz,1:iq2,1:kd3,1:lm2,1:mpo,1:slq,1:fpi,1:g53,1:gda,1:jwk,1:l3n,1:mj4,1:nhj,1:pa9,1:qh2,1:qjg,1:s1b,1:tv6,1:upb,1:v7e,1:nt8,1:nzz,1:otc,1:q15,1:t1g,1:vb3,1:rfa,1:hjz,1:iro,1:okg,1:pdq,1:mle,1:nuy,1:rsy,1:ttj,1:hjs,1:i8y,1:kyb,1:lp6,1:lxr,1:oq7,1:oxj,1:tor,1:gcy,1:pe3,1:g64,1:g5o,1:o4c,1:ou6,1:q29,1:tpx,1:rk0,1:jin,1:kxu,1:rge,1:fg9,1:i5j,1:iz6,1:mtj,1:n74,1:gev,1:nwb,1:fr3,1:iw9,1:ff1,1:lho,1:k1k,1:opu,1:gjn,1:hge,1:ni9,1:q4p,1:re2,1:la2,1:sds,1:lfs,1:jjy,1:put,1:jog,1:n6t,1:fji,1:g2x,1:g5l,1:kn5,1:oo7,1:prj,1:rf6,1:ssv,1:gdx,1:gn6,1:ho3,1:ir0,1:k3p,1:m4e,1:nxm,1:tkd,1:ui2,1:vbr,1:vda,1:gbf,1:kau,1:l87,1:sbm,1:ino,1:j9s,1:jcw,1:jve,1:lyb,1:mdl,1:n0p,1:o22,1:ovo,1:pdb,1:s9y,1:q9a,1:sgz,1:t3e,1:g7r,1:gbn,1:gql,1:ma0,1:r0i,1:re2,1:iq1,1:iyd,1:jnf,1:l9q,1:oji,1:mt1,1:fl0,1:h6h,1:i5j,1:iii,1:izg,1:muq,1:mzt,1:p7e,1:pc6,1:svo,1:tti,1:tvs,1:u1k,1:fpn,1:o1a,1:ss4,1:trs,1:j4x,1:fie,1:fyy,1:i5q,1:i7z,1:k2h,1:kxu,1:mha,1:ncy,1:q9o,1:sjg,1:vil,1:k94,1:tny,1:gax,1:kfv,1:kj7,1:lkx,1:luk,1:mx5,1:n6h,1:nsb,1:opc,1:u3i,1:g31,1:je6,1:tq0,1:fu3,1:ie1,1:lpm,1:sa2,1:ivf,1:j7o,1:kk7,1:mtj,1:tqe,1:g89,1:glr,1:i91,1:k5v,1:kem,1:ko8,1:lid,1:mx2,1:naa,1:p6h,1:r0v,1:r1d,1:sss,1:tsy,1:gfz,1:lve,1:gm5,1:mhc,1:n14,1:qgq,1:tr7,1:uvr,1:vbz,1:kgn,1:lor,1:pns,1:o97,1:obk,1:olu,1:mo0,1:m8p,1:riw,1:fk0,1:px2,1:g8q,1:g7b,1:ip2,1:jir,1:onq,1:i2t,1:ljy,1:kac,1:saj,1:qx8,1:tot,1:r63,1:iqb,1:g1s,1:gxs,3:hga,2:k8k,3:g66,1:mqy,1:ngq,1:ny4,1:nz2,1:nz9,1:o0f,1:tw6,1:on2,1:p8d,2:qhe,2:rg8,3:shk,1:sn1,4:u7j,1:u7w,1:u94,1:v5g,1:sm6,1:trr,1:fqm,1:fzb,1:g25,1:gcp,1:gdg,1:ggh,1:gwt,1:gzq,1:h2g,1:hfk,1:hig,1:i90,1:i9a,1:j2s,1:j8o,1:j9q,1:jci,1:k0f,1:k4i,1:k9d,1:kn9,1:lmf,1:lqy,1:lya,1:mem,1:mjv,1:mxu,1:ntd,1:ny6,1:nyh,1:nyg,1:nyo,1:nyu,1:nz1,1:o0d,1:o0e,1:o5c,1:o75,1:og0,1:ovo,1:ow9,1:oxt,1:p6a,1:pad,1:pnx,1:prt,1:prt,1:q53,1:r2o,1:r6e,1:rgh,1:ri1,1:rtf,1:ruw,1:sdy,1:shk,1:tsz,1:u0v,1:u2j,1:j1x,1:36cu,1:pp4,3:ffq,1:g51,1:g2w,1:fpc,1:g1x,1:g3k,1:gcn,1:ge2,1:gwt,1:gut,1:gwp,1:gyq,1:hga,1:hir,1:hms,1:hn8,1:huq,1:i08,1:iqa,1:iqh,1:iuh,1:iwd,1:j4o,1:j7y,1:j60,1:j9q,1:j8g,1:jci,1:jec,1:jpg,1:jrw,1:jte,1:k0m,1:k8k,1:kd3,1:kd7,1:keu,1:l89,1:la2,1:lkx,1:lwb,1:lvv,1:lya,1:m5a,1:mem,1:nmv,1:mk5,1:mnj,1:mqy,1:n01,1:n46,1:n5n,1:nbx,1:nbz,1:ngq,1:nh7,1:nhw,1:nka,1:nk0,1:nv0,1:o8h,1:og0,1:ol7,1:orf,1:ovo,1:p4u,1:pad,1:pxu,1:q27,1:qrd,1:r41,1:r5y,1:r6e,1:ren,1:rg8,1:rez,1:rgh,1:rge,1:rfx,1:ri1,1:rka,1:ruw,1:sag,1:sj6,1:srd,1:sxi,1:tq4,1:tsz,1:tw6,1:tzv,1:u0v,1:u17,1:u2j,1:umq,1:vj0,1:313e,1:3138,1:33dh,1:brx,1:cns,1:cop,1:39e1,1:3bgw,1:3i6r,1:vgj,1:vim,1w:15l.14k,2:16a.14n,b:161.14x,1:161.14y,1:161.14s.14x,1:161.14s.14y,1:15c.14n,1:15c.14o,1:15c.14s,1:15d.14s,1:15e.14s,1:15f.14s,1:15g.14s,1:15h.14s,1:15i.14s,2:15k.14s,1:15l.14s,1:15m.14s,1:15n.14s,1:15o.14s,2:15q.14s,2:15s.14s,1:15t.14s,2:15v.14s,1:15w.14s,2:15y.14s,1:15z.14s,1:160.14s,1:161.14s,1:162.14s,1:15h.14p,1:15d.14v,1:15n.14v,1:15w.14v,47g:1huh.1hve,2:1huj.1hve,f:1hut.1hve,3n:1hyp.1hyf,1:1hyq.1hyf,f0:1idj.1ida,1:1idj.1idz,a7:1int.1inu,1:1int.1ink,2:1int.1inx,70:1iuw.1iun,1:1iux.1iun,ot:1jjp.1jjk,10di:2jx3.2jxh,1:2jx4.2jxh,1:2jx4.2jxh.2jxq,1:2jx4.2jxh.2jxr,1:2jx4.2jxh.2jxs,1:2jx4.2jxh.2jxt,1:2jx4.2jxh.2jxu,2f:2jzt.2jxh,1:2jzu.2jxh,1:2jzt.2jxh.2jxq,1:2jzu.2jxh.2jxq,1:2jzt.2jxh.2jxr,1:2jzu.2jxh.2jxr,1m4g:fgd,1:fg8,1:fgh,1:2tcy,1:fog,1:fqm,1:fqz,1:fsy,1:fwa,1:fx5,1:fzb,1:fyn,1:ae6,1:2ud6,1:g25,1:g2c,1:g2s,1:g3b,1:2u58,1:aex,1:g2v,1:g3x,1:2u6j,1:g47,1:g4k,1:fkc,1:g4s,1:g51,1:3ly7,1:g6t,1:g77,1:afz,1:g8r,1:g92,1:gaa,1:gaf,1:ahh,1:gcn,1:gcp,1:gdg,1:ge2,1:ged,1:gee,1:gev,1:gg9,1:ggh,1:ggq,1:ghf,1:ghp,1:ghr,1:ghr,1:ghr,1:2v58,1:m7k,1:gju,1:gkf,1:2vdv,1:gkr,1:gkx,1:gli,1:gpq,1:gmw,1:gnc,1:go8,1:gpu,1:gs6,1:gsw,1:gur,1:gv7,1:gw4,1:gw4,1:gwp,1:gx7,1:gxf,1:gxu,1:h7a,1:gzq,1:h7b,1:h1t,1:h2s,1:g7b,1:hke,1:hce,1:hdg,1:he5,1:haj,1:hf6,1:hf5,1:hik,1:2x9g,1:hki,1:hkn,1:hl2,1:hlm,1:hlu,1:hnm,1:2xm0,1:2xnu,1:hrg,1:hsr,1:ht3,1:hqw,1:huu,1:aum,1:av0,1:hzc,1:i0u,1:i0u,1:2y88,1:i4j,1:i54,1:i5j,1:i5v,1:2yhk,1:i67,1:i6e,1:itv,1:i76,1:ayp,1:i8w,1:i9a,1:ibk,1:ia5,1:2z1g,1:if7,1:2z1i,1:ige,1:igb,1:igs,1:ijl,1:ijm,1:b3j,1:ikd,1:ilk,1:im5,1:ind,1:b4y,1:2zr7,1:b5o,1:ipc,1:ipf,1:ipi,1:iq2,1:3pg2,1:iri,1:3035,1:3035,1:poh,1:isi,1:isi,1:b7r,1:335k,1:3cgq,1:iua,1:iuj,1:b8j,1:ivu,1:ix9,1:ixj,1:iyh,1:j29,1:bay,1:ba4,1:j2s,1:30t0,1:j47,1:j7s,1:j7w,1:j7y,1:j7w,1:j96,1:j9q,1:jaq,1:jac,1:jan,1:jby,1:jci,1:jcm,1:jdc,1:jdn,1:jfh,1:jht,1:jis,1:jm8,1:31n0,1:jlp,1:jjw,1:jmw,1:jnn,1:jqc,1:31td,1:js2,1:jph,1:jop,1:bhq,1:ju1,1:jum,1:jvh,1:juf,1:bjg,1:k0f,1:k18,1:32mi,1:k4j,1:kc8,1:k7d,1:bo9,1:k9d,1:bns,1:bms,1:g42,1:g45,1:kcg,1:k9o,1:pf1,1:deh,1:kd3,1:kd7,1:kdd,1:kf2,1:ker,1:33cz,1:bpl,1:kje,1:kg5,1:klu,1:kn9,1:33hp,1:kni,1:kkf,1:kr8,1:brx,1:ksi,1:kv7,1:kx6,1:l2g,1:33xf,1:l3v,1:bvc,1:l5t,1:34br,1:l78,1:bwu,1:l82,1:l9b,1:la2,1:la3,1:34p9,1:2yvf,1:34sa,1:le6,1:354s,1:lhb,1:lhp,1:lev,1:ljq,1:lku,1:lmf,1:lkx,1:lm1,1:lmg,1:lmt,1:357i,1:lkk,1:lrj,1:lta,1:c37,1:lvv,1:lvr,1:35jl,1:lq1,1:m0e,1:35ni,1:35ou,1:m2u,1:m61,1:m5a,1:m57,1:c5y,1:m6i,1:m7x,1:m7r,1:m99,1:2u5h,1:mdh,1:368z,1:mfw,1:36i3,1:mjs,1:mk5,1:mkw,1:36yw,1:mm8,1:mmt,1:3779,1:37dg,1:mt6,1:mtn,1:cdo,1:mud,1:ce0,1:ce0,1:myv,1:mzg,1:n01,1:n0l,1:n2i,1:cgr,1:n50,1:386u,1:n5q,1:389e,1:n74,1:2zrz,1:nbk,1:38v5,1:38vs,1:38zo,1:cn0,1:cnc,1:nhw,1:394j,1:394i,1:395l,1:396b,1:nj2,1:nj3,1:nj3,1:nka,1:cop,1:nm3,1:cp2,1:cra,1:39r1,1:nri,1:nt8,1:nv0,1:ctf,1:3a5i,1:nyu,1:3a8q,1:3a9x,1:o0f,1:o2z,1:cvj,1:o5c,1:o5m,1:o5r,1:3at8,1:3b1j,1:3b1j,1:oa6,1:d1e,1:3b8r,1:og6,1:og9,1:d2f,1:3beo,1:onm,1:d5s,1:oo8,1:oo3,1:oow,1:3c06,1:orn,1:d8h,1:ouf,1:ow2,1:oxx,1:d9w,1:3ciw,1:3cjr,1:dax,1:3cnt,1:p6i,1:3cqm,1:p79,1:pa2,1:pad,1:3d22,1:3d43,1:pcw,1:3d7s,1:pdc,1:33a7,1:ded,1:pf6,1:phf,1:dfv,1:pj2,1:hx1,1:3dlz,1:3dmd,1:33bn,1:33bw,1:poh,1:pok,1:sda,1:dij,1:psh,1:psb,1:pst,1:gc3,1:ptd,1:ptf,1:ptp,1:puu,1:3ebg,1:put,1:pwd,1:pyb,1:q0d,1:pwj,1:q0t,1:q1z,1:q53,1:pxv,1:q16,1:q18,1:q1o,1:3eie,1:3eqz,1:3emt,1:dnv,1:q9d,1:q9f,1:qae,1:3g0a,1:qck,1:3f3g,1:dp9,1:dpd,1:3f75,1:3ff6,1:dpn,1:qj4,1:qjg,1:qjr,1:qjt,1:qll,1:qko,1:qoe,1:qn6,1:qrd,1:qp4,1:qqz,1:qrq,1:dsn,1:qu9,1:qv5,1:dtl,1:qxs,1:qxv,1:3giv,1:r13,1:r1a,1:dv9,1:r22,1:aez,1:3gz2,1:3h46,1:dz2,1:dzb,1:rds,1:rfx,1:rka,1:rpx,1:3hrc,1:rsb,1:rsx,1:rvf,1:rxz,1:3i9b,1:2upw,1:s0b,1:rzw,1:s1c,1:2uvy,1:s7o,1:sag,1:3jki,1:3jl9,1:sk4,1:smp,1:snl,1:3ju6,1:snv,1:svs,1:t07,1:t08,1:sxo,1:t89,1:t91,1:3ksa,1:tjf,1:ej9,1:tkn,1:3l2v,1:eli,1:ts3,1:iia,1:tur,1:3ltx,1:3lzu,1:epa,1:epi,1:u00,1:3mdm,1:er6,1:3mhi,1:u17,1:u17,1:u21,1:3mpi,1:u76,1:eur,1:u95,1:ucn,1:ude,1:uf2,1:ez2,1:3nsg,1:umq,1:uv4,1:v0d,1:f66,1:f71,1:v3b,1:3owe,1:f7c,1:3oxx,1:3p5a,1:3p8x,1:vcr,1:f9y,1:veh,1:vem,1:vet,1:vf3,1:vfa,1:vgb,1:3pxc"; +export const PRIMARY_COMPOSITES = "5c,1,1,1,1,1,2,1,1,1,1,1,1,1,1,2,1,1,1,1,1,3,1,1,1,1,3,1,1,1,1,1,2,1,1,1,1,1,1,1,1,2,1,1,1,1,1,3,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,4,1,1,1,2,1,1,1,1,1,5,1,1,1,1,1,4,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,y,1,e,1,t,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,4,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,9e,1,2,1,1,2,2,1,1,q,1,1,1,1,1,1,q,1,1,1,1,5,1,18,1,2,4,5,1,1,b,w,n,1,2,4,5,1,1,o,1,22,1,e,1,1,1,3,1,3,1,1,1,1,1,3,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,3,1,89,1,1,1,1,4a,2,h,gm,8,3,47,1,ak,3,1,20,1i,1,1,3g,3c,7,1,2,1,3j,1,1,3y,2,1,1,g8,25c,2,2,2,2,4,15,2,3,1,2,jh,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,3,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,2,5,1,1,1,2,1,1,2,2,1,1,1,1,1,1,4,1,1,1,1,3,1,1,1,1,1,2,1,1,1,1,1,1,2,1,5,1,1,2,1,1,2,2,bi,1,j,v,1,1,1h,5,3,o,2,r,3,3,2,n,2,b,1,1,1,1,3,1,3,1,7,1,3,1,3,1,z,1,1,1,1d,1,1,1,7,1,1,1,2n3,2,2,2,2,2,2,2,2,2,2,2,3,2,2,7,1,2,1,2,1,2,1,2,1,n,a,e,2,2,2,2,2,2,2,2,2,2,2,3,2,2,7,1,2,1,2,1,2,1,2,1,n,3,1,1,1,4,1864,2,f,3n,1,f0,1,a7,1,2,70,1,ot"; +export const CASE_FOLDING = "1t:2p,1:2q,1:2r,1:2s,1:2t,1:2u,1:2v,1:2w,1:2x,1:2y,1:2z,1:30,1:31,1:32,1:33,1:34,1:35,1:36,1:37,1:38,1:39,1:3a,1:3b,1:3c,1:3d,1:3e,2j:qk,b:68,1:69,1:6a,1:6b,1:6c,1:6d,1:6e,1:6f,1:6g,1:6h,1:6i,1:6j,1:6k,1:6l,1:6m,1:6n,1:6o,1:6p,1:6q,1:6r,1:6s,1:6t,1:6u,2:6w,1:6x,1:6y,1:6z,1:70,1:71,1:72,1:37.37,x:75,2:77,2:79,2:7b,2:7d,2:7f,2:7h,2:7j,2:7l,2:7n,2:7p,2:7r,2:7t,2:7v,2:7x,2:7z,2:81,2:83,2:85,2:87,2:89,2:8b,2:8d,2:8f,2:2x.lj,2:8j,2:8l,2:8n,3:8q,2:8s,2:8u,2:8w,2:8y,2:90,2:92,2:94,2:jg.32,1:97,2:99,2:9b,2:9d,2:9f,2:9h,2:9j,2:9l,2:9n,2:9p,2:9r,2:9t,2:9v,2:9x,2:9z,2:a1,2:a3,2:a5,2:a7,2:a9,2:ab,2:ad,2:af,2:73,1:ai,2:ak,2:am,2:37,2:gj,1:ar,2:at,2:gk,1:aw,2:gm,1:gn,1:b0,3:d9,1:gp,1:gr,1:b6,2:gw,1:gz,2:h5,1:h4,1:bd,4:hb,1:he,2:hh,1:bl,2:bn,2:bp,2:hs,1:bs,2:hv,3:bx,2:i0,1:c0,2:i2,1:i3,1:c4,2:c6,2:ia,1:c9,4:cd,8:cm,1:cm,2:cp,1:cp,2:cs,1:cs,2:cu,2:cw,2:cy,2:d0,2:d2,2:d4,2:d6,2:d8,3:db,2:dd,2:df,2:dh,2:dj,2:dl,2:dn,2:dp,2:dr,2:2y.lo,1:dv,1:dv,2:dx,2:b9,1:cf,1:e1,2:e3,2:e5,2:e7,2:e9,2:eb,2:ed,2:ef,2:eh,2:ej,2:el,2:en,2:ep,2:er,2:et,2:ev,2:ex,2:ez,2:f1,2:f3,2:bi,2:f7,2:f9,2:fb,2:fd,2:ff,2:fh,2:fj,2:fl,2:fn,8:8rp,1:fw,2:be,1:8rq,3:g2,2:ao,1:i1,1:i4,1:g7,2:g9,2:gb,2:gd,2:gf,6v:qh,17:oh,2:oj,4:on,9:s3,7:q4,2:q5,1:q6,1:q7,2:r0,2:r1,1:r2,1:qh.lk.ld,1:q9,1:qa,1:qb,1:qc,1:qd,1:qe,1:qf,1:qg,1:qh,1:qi,1:qj,1:qk,1:ql,1:qm,1:qn,1:qo,1:qp,2:qr,1:qs,1:qt,1:qu,1:qv,1:qw,1:qx,1:qy,1:qz,5:qt.lk.ld,i:qr,d:rb,1:qa,1:qg,4:qu,1:qo,2:rd,2:rf,2:rh,2:rj,2:rl,2:rn,2:rp,2:rr,2:rt,2:rv,2:rx,2:rz,2:qi,1:qp,3:qg,1:qd,2:s8,2:s2,1:sb,3:or,1:os,1:ot,1:uo,1:up,1:uq,1:ur,1:us,1:ut,1:uu,1:uv,1:uw,1:ux,1:uy,1:uz,1:v0,1:v1,1:v2,1:v3,1:ts,1:tt,1:tu,1:tv,1:tw,1:tx,1:ty,1:tz,1:u0,1:u1,1:u2,1:u3,1:u4,1:u5,1:u6,1:u7,1:u8,1:u9,1:ua,1:ub,1:uc,1:ud,1:ue,1:uf,1:ug,1:uh,1:ui,1:uj,1:uk,1:ul,1:um,1:un,1d:v5,2:v7,2:v9,2:vb,2:vd,2:vf,2:vh,2:vj,2:vl,2:vn,2:vp,2:vr,2:vt,2:vv,2:vx,2:vz,2:w1,a:wb,2:wd,2:wf,2:wh,2:wj,2:wl,2:wn,2:wp,2:wr,2:wt,2:wv,2:wx,2:wz,2:x1,2:x3,2:x5,2:x7,2:x9,2:xb,2:xd,2:xf,2:xh,2:xj,2:xl,2:xn,2:xp,2:xr,2:y7,1:xu,2:xw,2:xy,2:y0,2:y2,2:y4,2:y6,3:y9,2:yb,2:yd,2:yf,2:yh,2:yj,2:yl,2:yn,2:yp,2:yr,2:yt,2:yv,2:yx,2:yz,2:z1,2:z3,2:z5,2:z7,2:z9,2:zb,2:zd,2:zf,2:zh,2:zj,2:zl,2:zn,2:zp,2:zr,2:zt,2:zv,2:zx,2:zz,2:101,2:103,2:105,2:107,2:109,2:10b,2:10d,2:10f,2:10h,2:10j,2:10l,2:10n,2:10p,2:10r,2:10t,2:10v,3:129,1:12a,1:12b,1:12c,1:12d,1:12e,1:12f,1:12g,1:12h,1:12i,1:12j,1:12k,1:12l,1:12m,1:12n,1:12o,1:12p,1:12q,1:12r,1:12s,1:12t,1:12u,1:12v,1:12w,1:12x,1:12y,1:12z,1:130,1:131,1:132,1:133,1:134,1:135,1:136,1:137,1:138,1:139,1:13a,1d:12d.136,26x:8w0,1:8w1,1:8w2,1:8w3,1:8w4,1:8w5,1:8w6,1:8w7,1:8w8,1:8w9,1:8wa,1:8wb,1:8wc,1:8wd,1:8we,1:8wf,1:8wg,1:8wh,1:8wi,1:8wj,1:8wk,1:8wl,1:8wm,1:8wn,1:8wo,1:8wp,1:8wq,1:8wr,1:8ws,1:8wt,1:8wu,1:8wv,1:8ww,1:8wx,1:8wy,1:8wz,1:8x0,1:8x1,2:8x3,6:8x9,mj:3xs,1:3xt,1:3xu,1:3xv,1:3xw,1:3xx,1oj:tu,1:tw,1:u6,1:u9,1:ua,1:ua,1:ui,1:v7,1:wuj,8:3bk,1:3bl,1:3bm,1:3bn,1:3bo,1:3bp,1:3bq,1:3br,1:3bs,1:3bt,1:3bu,1:3bv,1:3bw,1:3bx,1:3by,1:3bz,1:3c0,1:3c1,1:3c2,1:3c3,1:3c4,1:3c5,1:3c6,1:3c7,1:3c8,1:3c9,1:3ca,1:3cb,1:3cc,1:3cd,1:3ce,1:3cf,1:3cg,1:3ch,1:3ci,1:3cj,1:3ck,1:3cl,1:3cm,1:3cn,1:3co,1:3cp,1:3cq,3:3ct,1:3cu,1:3cv,8x:5xd,2:5xf,2:5xh,2:5xj,2:5xl,2:5xn,2:5xp,2:5xr,2:5xt,2:5xv,2:5xx,2:5xz,2:5y1,2:5y3,2:5y5,2:5y7,2:5y9,2:5yb,2:5yd,2:5yf,2:5yh,2:5yj,2:5yl,2:5yn,2:5yp,2:5yr,2:5yt,2:5yv,2:5yx,2:5yz,2:5z1,2:5z3,2:5z5,2:5z7,2:5z9,2:5zb,2:5zd,2:5zf,2:5zh,2:5zj,2:5zl,2:5zn,2:5zp,2:5zr,2:5zt,2:5zv,2:5zx,2:5zz,2:601,2:603,2:605,2:607,2:609,2:60b,2:60d,2:60f,2:60h,2:60j,2:60l,2:60n,2:60p,2:60r,2:60t,2:60v,2:60x,2:60z,2:611,2:613,2:615,2:617,2:619,2:61b,2:61d,2:61f,2:61h,2:2w.mp,1:38.lk,1:3b.lm,1:3d.lm,1:2p.ji,1:601,3:37.37,2:61t,2:61v,2:61x,2:61z,2:621,2:623,2:625,2:627,2:629,2:62b,2:62d,2:62f,2:62h,2:62j,2:62l,2:62n,2:62p,2:62r,2:62t,2:62v,2:62x,2:62z,2:631,2:633,2:635,2:637,2:639,2:63b,2:63d,2:63f,2:63h,2:63j,2:63l,2:63n,2:63p,2:63r,2:63t,2:63v,2:63x,2:63z,2:641,2:643,2:645,2:647,2:649,2:64b,2:64d,2:64f,a:64g,1:64h,1:64i,1:64j,1:64k,1:64l,1:64m,1:64n,9:64w,1:64x,1:64y,1:64z,1:650,1:651,b:65c,1:65d,1:65e,1:65f,1:65g,1:65h,1:65i,1:65j,9:65s,1:65t,1:65u,1:65v,1:65w,1:65x,1:65y,1:65z,9:668,1:669,1:66a,1:66b,1:66c,1:66d,3:qt.lv,2:qt.lv.lc,2:qt.lv.ld,2:qt.lv.n6,3:66p,2:66r,2:66t,2:66v,9:674,1:675,1:676,1:677,1:678,1:679,1:67a,1:67b,h:64g.qh,1:64h.qh,1:64i.qh,1:64j.qh,1:64k.qh,1:64l.qh,1:64m.qh,1:64n.qh,1:64g.qh,1:64h.qh,1:64i.qh,1:64j.qh,1:64k.qh,1:64l.qh,1:64m.qh,1:64n.qh,1:65c.qh,1:65d.qh,1:65e.qh,1:65f.qh,1:65g.qh,1:65h.qh,1:65i.qh,1:65j.qh,1:65c.qh,1:65d.qh,1:65e.qh,1:65f.qh,1:65g.qh,1:65h.qh,1:65i.qh,1:65j.qh,1:674.qh,1:675.qh,1:676.qh,1:677.qh,1:678.qh,1:679.qh,1:67a.qh,1:67b.qh,1:674.qh,1:675.qh,1:676.qh,1:677.qh,1:678.qh,1:679.qh,1:67a.qh,1:67b.qh,3:67k.qh,1:q9.qh,1:q4.qh,2:q9.n6,1:q9.n6.qh,1:69c,1:69d,1:67k,1:67l,1:q9.qh,2:qh,4:67o.qh,1:qf.qh,1:q6.qh,2:qf.n6,1:qf.n6.qh,1:67m,1:67n,1:67o,1:67p,1:qf.qh,6:qh.lk.lc,1:qh.lk.ld,3:qh.n6,1:qh.lk.n6,1:6a8,1:6a9,1:67q,1:67r,7:qt.lk.lc,1:qt.lk.ld,1:qp.lv,2:qt.n6,1:qt.lk.n6,1:6ao,1:6ap,1:67u,1:67v,1:6at,6:67w.qh,1:qx.qh,1:r2.qh,2:qx.n6,1:qx.n6.qh,1:67s,1:67t,1:67w,1:67x,1:qx.qh,8a:qx,4:2z,1:6d,7:6ku,1a:6ls,1:6lt,1:6lu,1:6lv,1:6lw,1:6lx,1:6ly,1:6lz,1:6m0,1:6m1,1:6m2,1:6m3,1:6m4,1:6m5,1:6m6,1:6m7,k:6mc,mr:79s,1:79t,1:79u,1:79v,1:79w,1:79x,1:79y,1:79z,1:7a0,1:7a1,1:7a2,1:7a3,1:7a4,1:7a5,1:7a6,1:7a7,1:7a8,1:7a9,1:7aa,1:7ab,1:7ac,1:7ad,1:7ae,1:7af,1:7ag,1:7ah,1f5:8q8,1:8q9,1:8qa,1:8qb,1:8qc,1:8qd,1:8qe,1:8qf,1:8qg,1:8qh,1:8qi,1:8qj,1:8qk,1:8ql,1:8qm,1:8qn,1:8qo,1:8qp,1:8qq,1:8qr,1:8qs,1:8qt,1:8qu,1:8qv,1:8qw,1:8qx,1:8qy,1:8qz,1:8r0,1:8r1,1:8r2,1:8r3,1:8r4,1:8r5,1:8r6,1:8r7,1:8r8,1:8r9,1:8ra,1:8rb,1:8rc,1:8rd,1:8re,1:8rf,1:8rg,1:8rh,1:8ri,1:8rj,1d:8rl,2:h7,1:5tp,1:hp,3:8rs,2:8ru,2:8rw,2:gh,1:hd,1:gg,1:gi,2:8s3,3:8s6,9:fz,1:g0,1:8sh,2:8sj,2:8sl,2:8sn,2:8sp,2:8sr,2:8st,2:8sv,2:8sx,2:8sz,2:8t1,2:8t3,2:8t5,2:8t7,2:8t9,2:8tb,2:8td,2:8tf,2:8th,2:8tj,2:8tl,2:8tn,2:8tp,2:8tr,2:8tt,2:8tv,2:8tx,2:8tz,2:8u1,2:8u3,2:8u5,2:8u7,2:8u9,2:8ub,2:8ud,2:8uf,2:8uh,2:8uj,2:8ul,2:8un,2:8up,2:8ur,2:8ut,2:8uv,2:8ux,2:8uz,2:8v1,2:8v3,2:8v5,2:8v7,9:8vg,2:8vi,5:8vn,nym:wu9,2:wub,2:wud,2:wuf,2:wuh,2:wuj,2:wul,2:wun,2:wup,2:wur,2:wut,2:wuv,2:wux,2:wuz,2:wv1,2:wv3,2:wv5,2:wv7,2:wv9,2:wvb,2:wvd,2:wvf,2:wvh,k:ww1,2:ww3,2:ww5,2:ww7,2:ww9,2:wwb,2:wwd,2:wwf,2:wwh,2:wwj,2:wwl,2:wwn,2:wwp,2:wwr,3s:x0j,2:x0l,2:x0n,2:x0p,2:x0r,2:x0t,2:x0v,4:x0z,2:x11,2:x13,2:x15,2:x17,2:x19,2:x1b,2:x1d,2:x1f,2:x1h,2:x1j,2:x1l,2:x1n,2:x1p,2:x1r,2:x1t,2:x1v,2:x1x,2:x1z,2:x21,2:x23,2:x25,2:x27,2:x29,2:x2b,2:x2d,2:x2f,2:x2h,2:x2j,2:x2l,2:x2n,b:x2y,2:x30,2:5tl,1:x33,2:x35,2:x37,2:x39,2:x3b,5:x3g,2:h1,3:x3l,2:x3n,4:x3r,2:x3t,2:x3v,2:x3x,2:x3z,2:x41,2:x43,2:x45,2:x47,2:x49,2:h2,1:gs,1:gx,1:h8,1:h6,2:im,1:hz,1:il,1:xub,1:x4l,2:x4n,2:x4p,2:x4r,2:x4t,2:x4v,2:x4x,2:x4z,2:x3o,1:hu,1:5u6,1:x54,2:x56,7:x5d,6:x5j,2:x5l,t:x6e,or:3vk,1:3vl,1:3vm,1:3vn,1:3vo,1:3vp,1:3vq,1:3vr,1:3vs,1:3vt,1:3vu,1:3vv,1:3vw,1:3vx,1:3vy,1:3vz,1:3w0,1:3w1,1:3w2,1:3w3,1:3w4,1:3w5,1:3w6,1:3w7,1:3w8,1:3w9,1:3wa,1:3wb,1:3wc,1:3wd,1:3we,1:3wf,1:3wg,1:3wh,1:3wi,1:3wj,1:3wk,1:3wl,1:3wm,1:3wn,1:3wo,1:3wp,1:3wq,1:3wr,1:3ws,1:3wt,1:3wu,1:3wv,1:3ww,1:3wx,1:3wy,1:3wz,1:3x0,1:3x1,1:3x2,1:3x3,1:3x4,1:3x5,1:3x6,1:3x7,1:3x8,1:3x9,1:3xa,1:3xb,1:3xc,1:3xd,1:3xe,1:3xf,1:3xg,1:3xh,1:3xi,1:3xj,1:3xk,1:3xl,1:3xm,1:3xn,1:3xo,1:3xp,1:3xq,1:3xr,fnl:2u.2u,1:2u.2x,1:2u.30,1:2u.2u.2x,1:2u.2u.30,1:37.38,1:37.38,d:12s.12u,1:12s.12d,1:12s.12j,1:132.12u,1:12s.12l,sq:1ef5,1:1ef6,1:1ef7,1:1ef8,1:1ef9,1:1efa,1:1efb,1:1efc,1:1efd,1:1efe,1:1eff,1:1efg,1:1efh,1:1efi,1:1efj,1:1efk,1:1efl,1:1efm,1:1efn,1:1efo,1:1efp,1:1efq,1:1efr,1:1efs,1:1eft,1:1efu,xy:1fe0,1:1fe1,1:1fe2,1:1fe3,1:1fe4,1:1fe5,1:1fe6,1:1fe7,1:1fe8,1:1fe9,1:1fea,1:1feb,1:1fec,1:1fed,1:1fee,1:1fef,1:1feg,1:1feh,1:1fei,1:1fej,1:1fek,1:1fel,1:1fem,1:1fen,1:1feo,1:1fep,1:1feq,1:1fer,1:1fes,1:1fet,1:1feu,1:1fev,1:1few,1:1fex,1:1fey,1:1fez,1:1ff0,1:1ff1,1:1ff2,1:1ff3,3t:1fiw,1:1fix,1:1fiy,1:1fiz,1:1fj0,1:1fj1,1:1fj2,1:1fj3,1:1fj4,1:1fj5,1:1fj6,1:1fj7,1:1fj8,1:1fj9,1:1fja,1:1fjb,1:1fjc,1:1fjd,1:1fje,1:1fjf,1:1fjg,1:1fjh,1:1fji,1:1fjj,1:1fjk,1:1fjl,1:1fjm,1:1fjn,1:1fjo,1:1fjp,1:1fjq,1:1fjr,1:1fjs,1:1fjt,1:1fju,1:1fjv,4d:1fo7,1:1fo8,1:1fo9,1:1foa,1:1fob,1:1foc,1:1fod,1:1foe,1:1fof,1:1fog,1:1foh,2:1foj,1:1fok,1:1fol,1:1fom,1:1fon,1:1foo,1:1fop,1:1foq,1:1for,1:1fos,1:1fot,1:1fou,1:1fov,1:1fow,1:1fox,2:1foz,1:1fp0,1:1fp1,1:1fp2,1:1fp3,1:1fp4,1:1fp5,2:1fp7,1:1fp8,1d7:1h34,1:1h35,1:1h36,1:1h37,1:1h38,1:1h39,1:1h3a,1:1h3b,1:1h3c,1:1h3d,1:1h3e,1:1h3f,1:1h3g,1:1h3h,1:1h3i,1:1h3j,1:1h3k,1:1h3l,1:1h3m,1:1h3n,1:1h3o,1:1h3p,1:1h3q,1:1h3r,1:1h3s,1:1h3t,1:1h3u,1:1h3v,1:1h3w,1:1h3x,1:1h3y,1:1h3z,1:1h40,1:1h41,1:1h42,1:1h43,1:1h44,1:1h45,1:1h46,1:1h47,1:1h48,1:1h49,1:1h4a,1:1h4b,1:1h4c,1:1h4d,1:1h4e,1:1h4f,1:1h4g,1:1h4h,1:1h4i,2cu:1jgg,1:1jgh,1:1jgi,1:1jgj,1:1jgk,1:1jgl,1:1jgm,1:1jgn,1:1jgo,1:1jgp,1:1jgq,1:1jgr,1:1jgs,1:1jgt,1:1jgu,1:1jgv,1:1jgw,1:1jgx,1:1jgy,1:1jgz,1:1jh0,1:1jh1,1:1jh2,1:1jh3,1:1jh4,1:1jh5,1:1jh6,1:1jh7,1:1jh8,1:1jh9,1:1jha,1:1jhb,gw1:20dc,1:20dd,1:20de,1:20df,1:20dg,1:20dh,1:20di,1:20dj,1:20dk,1:20dl,1:20dm,1:20dn,1:20do,1:20dp,1:20dq,1:20dr,1:20ds,1:20dt,1:20du,1:20dv,1:20dw,1:20dx,1:20dy,1:20dz,1:20e0,1:20e1,1:20e2,1:20e3,1:20e4,1:20e5,1:20e6,1:20e7,o81:2oma,1:2omb,1:2omc,1:2omd,1:2ome,1:2omf,1:2omg,1:2omh,1:2omi,1:2omj,1:2omk,1:2oml,1:2omm,1:2omn,1:2omo,1:2omp,1:2omq,1:2omr,1:2oms,1:2omt,1:2omu,1:2omv,1:2omw,1:2omx,1:2omy,1:2omz,1:2on0,1:2on1,1:2on2,1:2on3,1:2on4,1:2on5,1:2on6,1:2on7"; diff --git a/packages/skill-tree/src/unicode-15.ts b/packages/skill-tree/src/unicode-15.ts new file mode 100644 index 0000000..9b3aee5 --- /dev/null +++ b/packages/skill-tree/src/unicode-15.ts @@ -0,0 +1,200 @@ +/** + * Unicode 15.0.0 character data for path validation: general category + * (assigned / Cf), canonical normalization (NFD, NFC) and full case folding + * (CaseFolding.txt statuses C and F). Everything is driven by tables + * generated from the 15.0.0 UCD files, never by the runtime's Intl or + * String.prototype.normalize data, so every runtime classifies a path + * identically (contract 4.3). + */ +import { + ASSIGNED_RANGES, + CANONICAL_DECOMPOSITIONS, + CASE_FOLDING, + COMBINING_CLASSES, + FORMAT_RANGES, + PRIMARY_COMPOSITES, + UNICODE_VERSION, +} from "./unicode-15-tables"; + +export { UNICODE_VERSION }; + +/** Decodes "delta.len" base-36 range items into flat [lo, hi, ...] pairs. */ +function decodeRanges(encoded: string): Int32Array { + const items = encoded.split(","); + const out = new Int32Array(items.length * 2); + let prev = 0; + items.forEach((item, i) => { + const [delta = "", len = ""] = item.split("."); + const lo = prev + parseInt(delta, 36); + const hi = lo + parseInt(len, 36); + out[i * 2] = lo; + out[i * 2 + 1] = hi; + prev = hi; + }); + return out; +} + +/** Decodes "deltaKey:v1.v2" items (key delta-coded, values absolute) into a map. */ +function decodeMap(encoded: string): Map { + const out = new Map(); + let prev = 0; + for (const item of encoded.split(",")) { + const [key = "", values = ""] = item.split(":"); + prev += parseInt(key, 36); + out.set(prev, values.split(".").map((v) => parseInt(v, 36))); + } + return out; +} + +function inRanges(ranges: Int32Array, cp: number): boolean { + let lo = 0; + let hi = ranges.length / 2 - 1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (cp < ranges[mid * 2]!) hi = mid - 1; + else if (cp > ranges[mid * 2 + 1]!) lo = mid + 1; + else return true; + } + return false; +} + +const assigned = decodeRanges(ASSIGNED_RANGES); +const format = decodeRanges(FORMAT_RANGES); +const decompositions = decodeMap(CANONICAL_DECOMPOSITIONS); +const folding = decodeMap(CASE_FOLDING); + +const combiningClasses = new Map(); +{ + let prev = 0; + for (const item of COMBINING_CLASSES.split(",")) { + const [delta = "", len = "", value = ""] = item.split("."); + const lo = prev + parseInt(delta, 36); + const hi = lo + parseInt(len, 36); + for (let cp = lo; cp <= hi; cp++) combiningClasses.set(cp, parseInt(value, 36)); + prev = hi; + } +} + +const compositions = new Map(); +{ + let prev = 0; + for (const delta of PRIMARY_COMPOSITES.split(",")) { + prev += parseInt(delta, 36); + const [a, b] = decompositions.get(prev) ?? []; + if (a !== undefined && b !== undefined) compositions.set(pairKey(a, b), prev); + } +} + +function pairKey(a: number, b: number): number { + return a * 0x110000 + b; +} + +/** General category is not Cn (Co and Cs count as assigned). */ +export function isAssigned(cp: number): boolean { + return inRanges(assigned, cp); +} + +/** General category Cf. */ +export function isFormat(cp: number): boolean { + return inRanges(format, cp); +} + +export function combiningClass(cp: number): number { + return combiningClasses.get(cp) ?? 0; +} + +/** The C+F case folding of one code point, or undefined when it has none. */ +export function caseFold(cp: number): readonly number[] | undefined { + return folding.get(cp); +} + +/** Every code point with a C or F case folding entry. */ +export function caseFoldEntries(): ReadonlyMap { + return folding; +} + +// Hangul syllable constants (Unicode 3.12). +const S_BASE = 0xac00, L_BASE = 0x1100, V_BASE = 0x1161, T_BASE = 0x11a7; +const L_COUNT = 19, V_COUNT = 21, T_COUNT = 28, N_COUNT = V_COUNT * T_COUNT, S_COUNT = L_COUNT * N_COUNT; + +function decomposeInto(cp: number, out: number[]): void { + const s = cp - S_BASE; + if (s >= 0 && s < S_COUNT) { + out.push(L_BASE + Math.floor(s / N_COUNT), V_BASE + Math.floor((s % N_COUNT) / T_COUNT)); + if (s % T_COUNT !== 0) out.push(T_BASE + (s % T_COUNT)); + return; + } + const mapping = decompositions.get(cp); + if (mapping === undefined) out.push(cp); + else for (const m of mapping) decomposeInto(m, out); +} + +/** Canonical decomposition with canonical ordering (NFD) over code points. */ +function nfdCodePoints(cps: Iterable): number[] { + const out: number[] = []; + for (const cp of cps) decomposeInto(cp, out); + // Canonical ordering: stable insertion sort of each run of non-starters. + for (let i = 1; i < out.length; i++) { + const cc = combiningClass(out[i]!); + if (cc === 0) continue; + let j = i; + while (j > 0 && combiningClass(out[j - 1]!) > cc) { + [out[j - 1], out[j]] = [out[j]!, out[j - 1]!]; + j--; + } + } + return out; +} + +function composePair(a: number, b: number): number | undefined { + const l = a - L_BASE, v = b - V_BASE; + if (l >= 0 && l < L_COUNT && v >= 0 && v < V_COUNT) return S_BASE + (l * V_COUNT + v) * T_COUNT; + const s = a - S_BASE, t = b - T_BASE; + if (s >= 0 && s < S_COUNT && s % T_COUNT === 0 && t > 0 && t < T_COUNT) return a + t; + return compositions.get(pairKey(a, b)); +} + +function nfcCodePoints(cps: Iterable): number[] { + const d = nfdCodePoints(cps); + if (d.length === 0) return d; + const out = [d[0]!]; + let starterPos = 0; + let lastClass = combiningClass(d[0]!) === 0 ? 0 : 256; + for (let i = 1; i < d.length; i++) { + const ch = d[i]!; + const chClass = combiningClass(ch); + const composite = composePair(out[starterPos]!, ch); + if (composite !== undefined && (lastClass < chClass || lastClass === 0)) { + out[starterPos] = composite; + continue; + } + if (chClass === 0) starterPos = out.length; + lastClass = chClass; + out.push(ch); + } + return out; +} + +const codePoints = (s: string): number[] => Array.from(s, (ch) => ch.codePointAt(0)!); +export function nfd(s: string): string { + return String.fromCodePoint(...nfdCodePoints(codePoints(s))); +} + +export function nfc(s: string): string { + return String.fromCodePoint(...nfcCodePoints(codePoints(s))); +} + +export function isNfc(s: string): boolean { + return nfc(s) === s; +} + +/** Full case folding (statuses C and F), code point by code point. */ +export function fold(s: string): string { + const out: number[] = []; + for (const cp of codePoints(s)) { + const mapped = folding.get(cp); + if (mapped === undefined) out.push(cp); + else out.push(...mapped); + } + return String.fromCodePoint(...out); +} diff --git a/packages/skill-tree/src/zip-reader.ts b/packages/skill-tree/src/zip-reader.ts new file mode 100644 index 0000000..d360735 --- /dev/null +++ b/packages/skill-tree/src/zip-reader.ts @@ -0,0 +1,147 @@ +/** + * Zip reader for `agentkit.skill-tree/v1` (schemas 2.1). The central + * directory is read first; the local entries must be exactly its entries, + * contiguous from offset 0 up to the central directory. Content is inflated + * and hashed as it streams, then checked against the central CRC-32. + */ +import { asTreeError, decompress, readFailed } from "./byte-source"; +import { EntryCollector, decodeName } from "./entry-collector"; +import { ERROR_CODES, treeError } from "./errors"; +import type { TreeManifest } from "./serialize"; +import { LIMITS, headerMode } from "./tree"; +import type { TreeLimits } from "./tree"; +import { + crc32, + parseDescriptor, + readAt, + readCentralDirectory, + readDirectoryEnd, + readLocalHeader, + toRandomAccess, +} from "./zip-records"; +import type { CentralEntry, LocalHeader, RandomAccessSource, Sizes } from "./zip-records"; + +const CHUNK = 64 * 1024; +const S_IFMT = 0o170000, S_IFREG = 0o100000, S_IFDIR = 0o040000; +const UNIX_HOST = 3; + +const sameBytes = (a: Uint8Array, b: Uint8Array) => a.length === b.length && a.every((x, i) => x === b[i]); + +/** Header agreement on CRC-32 and sizes (schemas 2.1); returns a disagreement or undefined. */ +function sizesDisagree(local: LocalHeader, central: CentralEntry, descriptor: Sizes | null): string | undefined { + const keys = ["crc", "csize", "usize"] as const; + if (central.flags & 0x8) { + if (descriptor === null) return "data descriptor is malformed"; + if (keys.some((k) => local[k] !== 0 && local[k] !== descriptor[k])) return "local header disagrees with its data descriptor"; + if (keys.some((k) => descriptor[k] !== central[k])) return "data descriptor disagrees with the central directory"; + return undefined; + } + return keys.some((k) => local[k] !== central[k]) ? "local header disagrees with the central directory" : undefined; +} + +function entryFault(central: CentralEntry, local: LocalHeader): string | undefined { + if (central.fault !== undefined || local.fault !== undefined) return central.fault ?? local.fault; + if ((central.flags | local.flags) & 0x41) return "entry is encrypted"; + if ((central.flags & 0x8) !== (local.flags & 0x8)) return "local header and central directory disagree on the data descriptor flag"; + if (central.method !== local.method) return "local header and central directory disagree on the method"; + if (central.method !== 0 && central.method !== 8) return `compression method ${central.method} is not stored or deflate`; + const type = (central.externalAttrs >>> 16) & S_IFMT; + if (central.madeBy >> 8 !== UNIX_HOST || (type !== S_IFREG && type !== S_IFDIR)) { + return "entry lacks Unix attributes of a regular file or directory"; + } + return undefined; +} + +async function* rawData(src: RandomAccessSource, start: number, length: number): AsyncGenerator { + for (let at = 0; at < length; at += CHUNK) yield await readAt(src, start + at, Math.min(CHUNK, length - at)); +} + +/** Content of an entry, inflated when deflated, with its CRC-32 and size checked at the end. */ +async function* content(src: RandomAccessSource, central: CentralEntry, dataStart: number, verify: boolean): AsyncGenerator { + const raw = rawData(src, dataStart, central.csize); + const data = central.method === 8 ? decompress(raw, "deflate-raw") : raw; + let crc = 0; + let size = 0; + for await (const chunk of data) { + crc = crc32(chunk, crc); + size += chunk.length; + yield chunk; + } + if (verify && (crc !== central.crc || size !== central.usize)) throw readFailed("zip entry data does not match its CRC-32 or size"); +} + +async function readEntries(src: RandomAccessSource, collector: EntryCollector, limits: TreeLimits): Promise { + const dir = await readDirectoryEnd(src); + if (dir.entries > limits.maxEntries) { + throw treeError(ERROR_CODES.limitExceeded, "", `archive has ${dir.entries} entries, more than ${limits.maxEntries}`); + } + const central = (await readCentralDirectory(src, dir)).sort((a, b) => a.localOffset - b.localOffset); + let archiveFault: string | undefined; + let pos = 0; + for (let i = 0; i < central.length; i++) { + const c = central[i]!; + collector.present(); + const name = decodeName(c.name); + if (c.localOffset !== pos) archiveFault ??= `bytes before or between local entries at offset ${pos}`; + const local = await readLocalHeader(src, c.localOffset, dir.cdOffset); + if (local === null || !sameBytes(local.name, c.name)) { + archiveFault ??= `no local entry ${JSON.stringify(name.path)} at offset ${c.localOffset}`; + collector.add({ path: name.path, utf8Valid: name.utf8Valid, mode: "", size: 0, sha256: "", kind: "special", reason: archiveFault }); + continue; + } + const dataEnd = local.dataStart + c.csize; + const nextStart = central[i + 1]?.localOffset ?? dir.cdOffset; + let fault = entryFault(c, local); + let descriptor: Sizes | null = null; + if (dataEnd > dir.cdOffset) { + archiveFault ??= `entry ${JSON.stringify(name.path)} data overruns the central directory`; + fault ??= archiveFault; + } else if (c.flags & 0x8) { + const length = nextStart - dataEnd; + descriptor = [12, 16, 20, 24].includes(length) ? parseDescriptor(await readAt(src, dataEnd, length)) : null; + pos = descriptor === null ? dataEnd : nextStart; + } else { + pos = dataEnd; + } + fault ??= sizesDisagree(local, c, descriptor); + const type = (c.externalAttrs >>> 16) & S_IFMT; + const base = { path: name.path, utf8Valid: name.utf8Valid, ...(fault === undefined ? {} : { reason: fault }) }; + if (type === S_IFDIR && fault === undefined) { + collector.add({ ...base, mode: "", size: 0, sha256: "", kind: "directory" }); + continue; + } + if (dataEnd > dir.cdOffset) { + collector.add({ ...base, mode: "", size: 0, sha256: "", kind: "special" }); + continue; + } + // Header agreement comes first; only agreeing entries are checked against their CRC-32. + const hashed = await collector.hashFile(name.path, content(src, c, local.dataStart, fault === undefined)); + collector.add({ ...base, mode: headerMode(c.externalAttrs >>> 16), ...hashed, kind: "regular" }); + } + if (pos !== dir.cdOffset) archiveFault ??= "bytes between the last local entry and the central directory"; + if (archiveFault !== undefined) { + collector.add({ path: "", mode: "", size: 0, sha256: "", kind: "special", reason: `zip archive is not contiguous: ${archiveFault}` }); + } +} + +export interface ZipReadOptions { + /** Test hook: smaller limits. */ + limits?: TreeLimits; +} + +/** + * Reads a zip archive into a validated tree manifest. Throws a + * `SkillTreeError`: unparseable records and corrupt data are + * `tree.read_failed`, header disagreements and non-contiguous entries are + * `tree.unsupported_entry`. + */ +export async function readZipTree(source: Uint8Array | Blob | RandomAccessSource, options: ZipReadOptions = {}): Promise { + const limits = options.limits ?? LIMITS; + const collector = new EntryCollector(limits); + try { + await readEntries(toRandomAccess(source), collector, limits); + } catch (err) { + throw asTreeError(err); + } + return collector.build(); +} diff --git a/packages/skill-tree/src/zip-records.ts b/packages/skill-tree/src/zip-records.ts new file mode 100644 index 0000000..fe0e556 --- /dev/null +++ b/packages/skill-tree/src/zip-records.ts @@ -0,0 +1,200 @@ +/** + * Zip record parsing for the skill-tree zip reader (schemas 2.1): end of + * central directory (with zip64), central directory entries, local headers + * and data descriptors. A record that is missing or cannot be parsed throws + * `tree.read_failed`; disagreements are judged by the reader. + */ +import { readFailed } from "./byte-source"; + +/** Random access to archive bytes, so the central directory can be read first. */ +export interface RandomAccessSource { + readonly size: number; + read(offset: number, length: number): Promise; +} + +export function toRandomAccess(source: Uint8Array | Blob | RandomAccessSource): RandomAccessSource { + if (source instanceof Uint8Array) { + return { size: source.length, read: async (offset, length) => source.subarray(offset, offset + length) }; + } + if (typeof Blob !== "undefined" && source instanceof Blob) { + return { size: source.size, read: async (o, l) => new Uint8Array(await source.slice(o, o + l).arrayBuffer()) }; + } + return source as RandomAccessSource; +} + +export async function readAt(src: RandomAccessSource, offset: number, length: number): Promise { + if (offset < 0 || length < 0 || offset + length > src.size) throw readFailed(`zip read past the end at ${offset}`); + const bytes = await src.read(offset, length); + if (bytes.length !== length) throw readFailed(`zip read returned ${bytes.length} of ${length} bytes`); + return bytes; +} + +const view = (b: Uint8Array) => new DataView(b.buffer, b.byteOffset, b.byteLength); +const u16 = (b: Uint8Array, o: number) => view(b).getUint16(o, true); +const u32 = (b: Uint8Array, o: number) => view(b).getUint32(o, true); +function u64(b: Uint8Array, o: number): number { + const value = view(b).getBigUint64(o, true); + if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw readFailed("zip64 value overflows"); + return Number(value); +} + +export const MAX32 = 0xffffffff; +const SIG_LOCAL = 0x04034b50, SIG_CENTRAL = 0x02014b50, SIG_EOCD = 0x06054b50; +const SIG_EOCD64 = 0x06064b50, SIG_LOCATOR64 = 0x07064b50, SIG_DESCRIPTOR = 0x08074b50; +/** Reading a larger central directory is refused rather than buffered. */ +const MAX_CENTRAL_DIRECTORY_BYTES = 64 * 1024 * 1024; + +export interface Directory { + entries: number; + cdOffset: number; + cdSize: number; +} + +/** Finds the end of central directory record (and the zip64 record when it is used), like Go archive/zip. */ +export async function readDirectoryEnd(src: RandomAccessSource): Promise { + const tailStart = Math.max(0, src.size - (22 + 0xffff)); + const tail = await readAt(src, tailStart, src.size - tailStart); + let at = -1; + for (let i = tail.length - 22; i >= 0; i--) { + if (u32(tail, i) === SIG_EOCD && i + 22 + u16(tail, i + 20) <= tail.length) { + at = i; + break; + } + } + if (at < 0) throw readFailed("zip end of central directory record not found"); + if (u16(tail, at + 4) !== 0 || u16(tail, at + 6) !== 0) throw readFailed("multi-disk zip archives are not supported"); + let dir: Directory = { entries: u16(tail, at + 10), cdSize: u32(tail, at + 12), cdOffset: u32(tail, at + 16) }; + let dirEnd = tailStart + at; + if ((dir.entries === 0xffff || dir.cdSize === MAX32 || dir.cdOffset === MAX32) && dirEnd >= 20) { + const locator = await readAt(src, dirEnd - 20, 20); + if (u32(locator, 0) === SIG_LOCATOR64) { + const offset = u64(locator, 8); + const record = await readAt(src, offset, 56); + if (u32(record, 0) !== SIG_EOCD64) throw readFailed("bad zip64 end of central directory signature"); + dir = { entries: u64(record, 32), cdSize: u64(record, 40), cdOffset: u64(record, 48) }; + dirEnd = offset; + } + } + if (dir.cdOffset + dir.cdSize > dirEnd) throw readFailed("zip central directory overlaps its end record"); + if (dir.cdSize > MAX_CENTRAL_DIRECTORY_BYTES) throw readFailed("zip central directory is too large"); + return dir; +} + +export interface Sizes { + crc: number; + csize: number; + usize: number; +} + +export interface CentralEntry extends Sizes { + name: Uint8Array; + madeBy: number; + flags: number; + method: number; + externalAttrs: number; + localOffset: number; + /** Set when a 0xFFFFFFFF field has no zip64 extra value. */ + fault?: string; +} + +/** Replaces 0xFFFFFFFF fields, in order, with the zip64 extra (0x0001) values. */ +function applyZip64(extra: Uint8Array, fields: number[]): string | undefined { + let values: Uint8Array | undefined; + for (let p = 0; p + 4 <= extra.length; ) { + const id = u16(extra, p); + const len = u16(extra, p + 2); + if (p + 4 + len > extra.length) throw readFailed("zip extra field overruns its record"); + if (id === 0x0001) values = extra.subarray(p + 4, p + 4 + len); + p += 4 + len; + } + let next = 0; + for (let i = 0; i < fields.length; i++) { + if (fields[i] !== MAX32) continue; + if (values === undefined || next + 8 > values.length) return "a 0xFFFFFFFF field has no zip64 extra value"; + fields[i] = u64(values, next); + next += 8; + } + return undefined; +} + +export async function readCentralDirectory(src: RandomAccessSource, dir: Directory): Promise { + const cd = await readAt(src, dir.cdOffset, dir.cdSize); + const entries: CentralEntry[] = []; + let p = 0; + for (let i = 0; i < dir.entries; i++) { + if (p + 46 > cd.length || u32(cd, p) !== SIG_CENTRAL) throw readFailed("bad zip central directory record"); + const nameLen = u16(cd, p + 28), extraLen = u16(cd, p + 30), commentLen = u16(cd, p + 32); + const end = p + 46 + nameLen + extraLen + commentLen; + if (end > cd.length) throw readFailed("zip central directory record overruns the directory"); + const fields = [u32(cd, p + 24), u32(cd, p + 20), u32(cd, p + 42)]; // usize, csize, offset + const fault = applyZip64(cd.subarray(p + 46 + nameLen, p + 46 + nameLen + extraLen), fields); + entries.push({ + name: cd.subarray(p + 46, p + 46 + nameLen), + madeBy: u16(cd, p + 4), + flags: u16(cd, p + 8), + method: u16(cd, p + 10), + crc: u32(cd, p + 16), + usize: fields[0]!, + csize: fields[1]!, + localOffset: fields[2]!, + externalAttrs: u32(cd, p + 38), + ...(fault === undefined ? {} : { fault }), + }); + p = end; + } + return entries; +} + +export interface LocalHeader extends Sizes { + name: Uint8Array; + flags: number; + method: number; + /** Offset of the entry data. */ + dataStart: number; + fault?: string; +} + +/** Reads the local header at `offset`, or returns null when no local header signature is there. */ +export async function readLocalHeader(src: RandomAccessSource, offset: number, limit: number): Promise { + if (offset + 30 > limit) return null; + const fixed = await readAt(src, offset, 30); + if (u32(fixed, 0) !== SIG_LOCAL) return null; + const nameLen = u16(fixed, 26), extraLen = u16(fixed, 28); + if (offset + 30 + nameLen + extraLen > limit) return null; + const rest = await readAt(src, offset + 30, nameLen + extraLen); + const fields = [u32(fixed, 22), u32(fixed, 18)]; // usize, csize + const fault = applyZip64(rest.subarray(nameLen), fields); + return { + name: rest.subarray(0, nameLen), + flags: u16(fixed, 6), + method: u16(fixed, 8), + crc: u32(fixed, 14), + usize: fields[0]!, + csize: fields[1]!, + dataStart: offset + 30 + nameLen + extraLen, + ...(fault === undefined ? {} : { fault }), + }; +} + +/** Parses a data descriptor whose length (12, 16, 20 or 24) the contiguity rule fixed; null when malformed. */ +export function parseDescriptor(bytes: Uint8Array): Sizes | null { + const signed = bytes.length === 16 || bytes.length === 24; + if (signed && u32(bytes, 0) !== SIG_DESCRIPTOR) return null; + const b = signed ? bytes.subarray(4) : bytes; + if (b.length === 12) return { crc: u32(b, 0), csize: u32(b, 4), usize: u32(b, 8) }; + if (b.length === 20) return { crc: u32(b, 0), csize: u64(b, 4), usize: u64(b, 12) }; + return null; +} + +const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, n) => { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + return c >>> 0; +}); + +/** Incremental CRC-32 (IEEE): pass the previous value to continue. */ +export function crc32(data: Uint8Array, previous = 0): number { + let c = ~previous >>> 0; + for (const byte of data) c = CRC_TABLE[(c ^ byte) & 0xff]! ^ (c >>> 8); + return ~c >>> 0; +} diff --git a/packages/skill-tree/test/identity-vectors.test.ts b/packages/skill-tree/test/identity-vectors.test.ts new file mode 100644 index 0000000..640f817 --- /dev/null +++ b/packages/skill-tree/test/identity-vectors.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from "vitest"; +import { + IDENTITY_PATTERN, + SkillTreeError, + checkAlias, + checkDirectoryAlias, + checkPublisher, + directoryAliasKey, + frontmatterExposure, + kitIdentity, + skillFileExposure, + trustOf, + validateIdentity, +} from "../src/index"; +import { fixtureBytes } from "./vector-helpers"; + +interface IdentityVectors { + schema: string; + spec: string; + notes: string[]; + pattern: string; + kitMapping: Array<{ kit: string; dir: string; identity?: string; portableAlias?: string; error?: string }>; + valid: string[]; + invalid: Array<{ value: string; error: string }>; + provenance: Array<{ + identity: string; + provider: string; + origin?: string; + verified?: boolean; + overlay?: boolean; + expect: { trust?: string; error?: string }; + }>; + aliases: Array<{ identity: string; alias: string; error: string | null }>; + directoryAliases: Array<{ identity: string; alias: string; error: string | null; groupKey?: string }>; + directoryAliasCollisions: Array<{ aliases: string[]; collide: boolean }>; + exposure: Array<{ frontmatter?: string; file?: string; exposure: string; warning: boolean }>; +} + +const v = JSON.parse(fixtureBytes("vectors/identity-v1-vectors.json").toString("utf8")) as IdentityVectors; + +/** The contract code a call throws, or null when it succeeds. */ +function codeOf(run: () => unknown): string | null { + try { + run(); + return null; + } catch (err) { + if (err instanceof SkillTreeError) return err.code; + throw err; + } +} + +describe("identity-v1 vectors (vendored unedited from AgentKit)", () => { + it("is the expected vector file with every section", () => { + expect(v.schema).toBe("agentkit.identity-vectors/v1"); + // RegExp.source escapes "/", so compare the compiled patterns. + expect(new RegExp(v.pattern).source).toBe(IDENTITY_PATTERN.source); + for (const section of [v.kitMapping, v.valid, v.invalid, v.provenance, v.aliases, v.directoryAliases, v.directoryAliasCollisions, v.exposure]) { + expect(section.length).toBeGreaterThan(0); + } + }); + + it("maps kit skill directories", () => { + for (const c of v.kitMapping) { + if (c.error !== undefined) expect(codeOf(() => kitIdentity(c.dir)), c.dir).toBe(c.error); + else expect(kitIdentity(c.dir), c.dir).toEqual({ identity: c.identity, alias: c.portableAlias }); + } + }); + + it("validates identities", () => { + for (const id of v.valid) expect(codeOf(() => validateIdentity(id)), id).toBeNull(); + for (const c of v.invalid) { + expect(codeOf(() => validateIdentity(c.value)), c.value).toBe(c.error); + expect(codeOf(() => checkPublisher(c.value, "kit")), c.value).toBe(c.error); + } + }); + + it("derives trust or rejects provenance in contract order", () => { + for (const c of v.provenance) { + const p = { provider: c.provider, origin: c.origin ?? "", verified: c.verified ?? false, overlay: c.overlay ?? false }; + const label = `${c.identity} ${JSON.stringify(p)}`; + if (c.expect.error !== undefined) expect(codeOf(() => trustOf(c.identity, p)), label).toBe(c.expect.error); + else expect(trustOf(c.identity, p), label).toBe(c.expect.trust); + } + }); + + it("checks invocation aliases", () => { + for (const c of v.aliases) expect(codeOf(() => checkAlias(c.identity, c.alias)), `${c.identity} ${c.alias}`).toBe(c.error); + }); + + it("checks directory aliases and their group keys", () => { + for (const c of v.directoryAliases) { + expect(codeOf(() => checkDirectoryAlias(c.identity, c.alias)), `${c.identity} ${c.alias}`).toBe(c.error); + if (c.error === null) expect(directoryAliasKey(c.alias)).toBe(c.groupKey); + } + for (const c of v.directoryAliasCollisions) { + const [a = "", b = ""] = c.aliases; + expect(directoryAliasKey(a) === directoryAliasKey(b), c.aliases.join(" ")).toBe(c.collide); + } + }); + + it("derives exposure from frontmatter and whole skill files", () => { + for (const c of v.exposure) { + expect((c.frontmatter === undefined) !== (c.file === undefined)).toBe(true); + const got = c.frontmatter !== undefined ? frontmatterExposure(c.frontmatter) : skillFileExposure(c.file ?? ""); + expect(got, JSON.stringify(c.frontmatter ?? c.file)).toEqual({ exposure: c.exposure, warning: c.warning }); + } + }); +}); + +describe("identity rules beyond the vectors", () => { + it("never folds look-alikes into reserved aliases", () => { + for (const alias of ["aK-plan", "аk-plan", "plan-é", "ık-plan", "/аk:plan"]) { + expect(codeOf(() => checkAlias("@evil/plan", alias)), alias).toBe("identity.invalid"); + expect(codeOf(() => checkAlias("@agentkit/plan", alias)), alias).toBe("identity.invalid"); + } + }); + + it("reserves ak- and ak: segments after leading / and $", () => { + for (const alias of ["$AK:plan", "/AK:plan", "$ak-plan", "x/$ak-plan", "core/$$AK-plan", "a/b/Ak:c"]) { + expect(codeOf(() => checkAlias("@acme/plan", alias)), alias).toBe("identity.reserved_alias"); + } + for (const alias of ["a$ak:plan", "x/bak-plan", "akx/plan", "ak/plan"]) { + expect(codeOf(() => checkAlias("@acme/plan", alias)), alias).toBeNull(); + } + }); + + it("checks the provider before the reserved publisher", () => { + expect(codeOf(() => trustOf("@agentkit/plan", { provider: "npm" }))).toBe("identity.invalid"); + expect(codeOf(() => trustOf("@agentkit/plan", { provider: "skillx", verified: true }))).toBe("identity.reserved_publisher"); + expect(codeOf(() => checkPublisher("@agentkit2/plan", "skillx"))).toBeNull(); + }); +}); diff --git a/packages/skill-tree/test/path-fuzz.test.ts b/packages/skill-tree/test/path-fuzz.test.ts new file mode 100644 index 0000000..a937304 --- /dev/null +++ b/packages/skill-tree/test/path-fuzz.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import { decodeName } from "../src/entry-collector"; +import { buildTree, frontmatterExposure, validatePath } from "../src/index"; +import { errorCodeOf } from "./vector-helpers"; + +const PATH_CODES = new Set(["tree.unsafe_path", "tree.non_nfc_path"]); + +/** Deterministic xorshift32, so a failure reproduces from the printed seed. */ +function rng(seed: number): () => number { + let s = seed >>> 0 || 1; + return () => { + s ^= s << 13; + s ^= s >>> 17; + s ^= s << 5; + return (s >>> 0) / 0x100000000; + }; +} + +const ALPHABET = ["a", "A", "/", ".", "..", "~1", " ", "\\", ":", "\u0000", "é", "é", "K", "​", "\ud800", "CON", "ß", "\u{1f600}", ".ak-tmp.", "$"]; + +describe("path validation never throws", () => { + it("classifies random byte names", () => { + const next = rng(0x5eed); + for (let i = 0; i < 20_000; i++) { + const bytes = Uint8Array.from({ length: Math.floor(next() * 40) }, () => Math.floor(next() * 256)); + const name = decodeName(bytes); + const result = validatePath(name.path); + if (result !== null && !PATH_CODES.has(result.code)) expect.fail(`seed 0x5eed #${i}: ${result.code}`); + } + }); + + it("classifies random token names and agrees with buildTree", async () => { + const next = rng(0xc0ffee); + for (let i = 0; i < 5_000; i++) { + const path = Array.from({ length: 1 + Math.floor(next() * 6) }, () => ALPHABET[Math.floor(next() * ALPHABET.length)]).join(""); + const result = validatePath(path); + if (result !== null && !PATH_CODES.has(result.code)) expect.fail(`${JSON.stringify(path)}: ${result.code}`); + const entry = { path, mode: "100644", size: 1, sha256: "0".repeat(64), kind: "regular" as const }; + const code = await errorCodeOf(() => buildTree([entry])); + if (result !== null) expect(code, JSON.stringify(path)).toBe(result.code); + // A valid path still fails as a whole tree only when it is noise (empty tree). + else expect(code === null || code === "tree.empty", JSON.stringify(path)).toBe(true); + } + }); +}); + +describe("frontmatter exposure never throws", () => { + it.each([ + ["", "AUTO", false], + ["user-invocable: false", "INTERNAL", false], + ["disable-model-invocation: TRUE", "EXPLICIT", false], + // A quoted value is a string, not a boolean: it is ignored without a warning. + ["user-invocable: 'false'", "AUTO", false], + ["user-invocable: !!bool false", "AUTO", true], + ["user-invocable: &a false", "AUTO", true], + ["user-invocable: false\nuser-invocable: true", "AUTO", true], + ["- a\n- b", "AUTO", true], + ["key: [unclosed", "AUTO", true], + ] as const)("%j", (text, exposure, warning) => { + expect(frontmatterExposure(text)).toEqual({ exposure, warning }); + }); + + it("handles random input", () => { + const next = rng(0xfeed); + const pieces = ["user-invocable", "disable-model-invocation", ":", " ", "\n", "true", "false", "!!", "&", "*", "'", '"', "[", "{", "- ", "#"]; + for (let i = 0; i < 5_000; i++) { + const text = Array.from({ length: Math.floor(next() * 12) }, () => pieces[Math.floor(next() * pieces.length)]).join(""); + const got = frontmatterExposure(text); + expect(["AUTO", "EXPLICIT", "INTERNAL"]).toContain(got.exposure); + } + }); +}); diff --git a/packages/skill-tree/test/provenance.test.ts b/packages/skill-tree/test/provenance.test.ts new file mode 100644 index 0000000..c38bb59 --- /dev/null +++ b/packages/skill-tree/test/provenance.test.ts @@ -0,0 +1,104 @@ +/** + * Pins the vendored fixtures and keeps the generated Unicode tables in sync + * with their pinned inputs (the AgentKit CaseFolding file and the Go tables + * dump), so table drift fails here instead of silently changing digests. + */ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { UNICODE_VERSION, caseFoldEntries, combiningClass, isAssigned, isFormat, nfc, nfd } from "../src/unicode-15"; +import { fixtureBytes, sha256 } from "./vector-helpers"; + +const PINS: Record = { + "vectors/skill-tree-v1-vectors.json": "8eb46f1b8bec49cace740b75614f28f8fb4a6fc266fecfcd7e3d6892f28f3172", + "vectors/identity-v1-vectors.json": "1ab9e013c229d78871abd60e3cd3bf26fcfd0e6067d98dcdd79b4f6bcc1daefb", + "ucd/CaseFolding-15.0.0.txt": "cdd49e55eae3bbf1f0a3f6580c974a0263cb86a6a08daa10fbf705b4808a56f7", +}; +const GO_TABLES_SHA256 = "baa335155dcbaf0cfe8f40fcc0a98232c4134e821a08c3f198f829a51d911ad5"; +const MAX_CP = 0x10ffff; + +interface GoTables { + unicodeVersion: string; + normVersion: string; + assigned: Array<[number, number]>; + format: Array<[number, number]>; + combiningClass: Array<[number, number]>; + composites: number[]; +} + +const goBytes = readFileSync(fileURLToPath(new URL("../scripts/unicode-15.0.0-go.json", import.meta.url))); +const go = JSON.parse(goBytes.toString("utf8")) as GoTables; + +function rangeSet(ranges: Array<[number, number]>): Uint8Array { + const set = new Uint8Array(MAX_CP + 1); + for (const [lo, hi] of ranges) set.fill(1, lo, hi + 1); + return set; +} + +const isSurrogate = (cp: number) => cp >= 0xd800 && cp <= 0xdfff; + +describe("fixture provenance", () => { + it.each(Object.entries(PINS))("%s is the pinned upstream file", (path, pin) => { + expect(sha256(fixtureBytes(path))).toBe(pin); + }); + + it("the Go tables dump is the pinned Unicode 15.0.0 input", () => { + expect(sha256(goBytes)).toBe(GO_TABLES_SHA256); + expect([go.unicodeVersion, go.normVersion, UNICODE_VERSION]).toEqual(["15.0.0", "15.0.0", "15.0.0"]); + }); +}); + +describe("generated tables stay in sync with their inputs", () => { + it("case folding equals CaseFolding.txt statuses C and F", () => { + const want = new Map(); + for (const line of fixtureBytes("ucd/CaseFolding-15.0.0.txt").toString("utf8").split("\n")) { + const fields = line.replace(/#.*/, "").split(";").map((f) => f.trim()); + if (fields.length < 3 || (fields[1] !== "C" && fields[1] !== "F")) continue; + want.set(parseInt(fields[0]!, 16), fields[2]!.split(" ").map((h) => parseInt(h, 16))); + } + expect(want.size).toBeGreaterThan(1400); + expect(new Map(caseFoldEntries())).toEqual(want); + }); + + it("general categories match the Go tables for every code point", () => { + const assigned = rangeSet(go.assigned); + const format = rangeSet(go.format); + for (let cp = 0; cp <= MAX_CP; cp++) { + if (isAssigned(cp) !== (assigned[cp] === 1)) expect.fail(`assigned U+${cp.toString(16)}`); + if (isFormat(cp) !== (format[cp] === 1)) expect.fail(`format U+${cp.toString(16)}`); + } + }); + + it("combining classes match the Go tables", () => { + const want = new Map(go.combiningClass); + for (let cp = 0; cp <= MAX_CP; cp++) { + if (combiningClass(cp) !== (want.get(cp) ?? 0)) expect.fail(`ccc U+${cp.toString(16)}`); + } + }); +}); + +describe("normalization agrees with the runtime on Unicode 15.0.0 characters", () => { + it("NFD and NFC of every assigned code point", () => { + let checked = 0; + for (let cp = 0; cp <= MAX_CP; cp++) { + if (!isAssigned(cp) || isSurrogate(cp)) continue; + const ch = String.fromCodePoint(cp); + if (nfd(ch) !== ch.normalize("NFD")) expect.fail(`NFD U+${cp.toString(16)}`); + if (nfc(ch) !== ch.normalize("NFC")) expect.fail(`NFC U+${cp.toString(16)}`); + checked++; + } + expect(checked).toBeGreaterThan(149_000); + }); + + it("composes, reorders and blocks sequences", () => { + const samples = [ + "é", "Ǻ", "ḍ̇", "q̣̇", "각", "각", + "ą́", "Ą́", "אַּ", "क़", "Å", "Ω", + "ཱི", "̈́", "à̛̀̕", "Ą̊", "ᅡᆨ", "́e", + ]; + for (const s of samples) { + expect(nfd(s), JSON.stringify(s)).toBe(s.normalize("NFD")); + expect(nfc(s), JSON.stringify(s)).toBe(s.normalize("NFC")); + } + }); +}); diff --git a/packages/skill-tree/test/tar-reader.test.ts b/packages/skill-tree/test/tar-reader.test.ts new file mode 100644 index 0000000..ae2826b --- /dev/null +++ b/packages/skill-tree/test/tar-reader.test.ts @@ -0,0 +1,153 @@ +import { once } from "node:events"; +import { createGzip, gzipSync } from "node:zlib"; +import { describe, expect, it } from "vitest"; +import { buildTree, readTarTree, readZipTree } from "../src/index"; +import { tarBytes, tarStream } from "./tar-writer"; +import type { TarSpec } from "./tar-writer"; +import { zipBytes } from "./zip-writer"; +import { errorCodeOf, sha256 } from "./vector-helpers"; + +const enc = new TextEncoder(); +const SKILL = enc.encode("---\nname: demo\ndescription: Demo.\n---\n"); + +function file(name: string, data: Uint8Array | string = "x\n", mode = 0o644): TarSpec { + return { name: enc.encode(name), typeflag: "0", mode, data: typeof data === "string" ? enc.encode(data) : data }; +} + +const skill = file("SKILL.md", SKILL); + +/** Gzips a stream without holding its uncompressed bytes. */ +async function gzipStream(source: AsyncIterable): Promise { + const parts: Buffer[] = []; + const gz = createGzip(); + const drained = (async () => { + for await (const chunk of gz) parts.push(chunk as Buffer); + })(); + for await (const chunk of source) if (!gz.write(chunk)) await once(gz, "drain"); + gz.end(); + await drained; + return Buffer.concat(parts); +} + +async function tarCode(specs: TarSpec[]): Promise { + return errorCodeOf(() => readTarTree(tarStream(specs))); +} + +describe("tar reader rejects unsafe entries", () => { + it.each([ + ["zip-slip parent segment", [file("../evil.md")], "tree.unsafe_path"], + ["nested parent segment", [file("refs/../../evil.md")], "tree.unsafe_path"], + ["absolute path", [file("/etc/passwd")], "tree.unsafe_path"], + ["backslash", [file("refs\\x.md")], "tree.unsafe_path"], + ["Windows reserved name", [file("CON.md")], "tree.unsafe_path"], + ["8.3 short name", [file("PROGRA~1/x.md")], "tree.unsafe_path"], + ["trailing dot", [file("notes.")], "tree.unsafe_path"], + ["NFD name", [file("café.md")], "tree.non_nfc_path"], + ["Kelvin sign (NFC maps it to K)", [file("K.md")], "tree.non_nfc_path"], + ["full case-fold collision", [file("STRASSE.md"), file("straße.md")], "tree.case_collision"], + ["simple case collision", [file("Refs/a.md"), file("refs/b.md")], "tree.case_collision"], + ["symbolic link", [{ name: enc.encode("link"), typeflag: "2", mode: 0o777 }], "tree.unsupported_entry"], + ["hard link", [{ name: enc.encode("hard"), typeflag: "1", mode: 0o644 }], "tree.unsupported_entry"], + ["FIFO", [{ name: enc.encode("pipe"), typeflag: "6", mode: 0o644 }], "tree.unsupported_entry"], + ["contiguous file", [{ ...file("big.bin"), typeflag: "7" }], "tree.unsupported_entry"], + ["sparse PAX file", [{ ...file("sparse.bin"), pax: { "GNU.sparse.major": "1", "GNU.sparse.minor": "0" } }], "tree.unsupported_entry"], + ["setuid file", [file("run.sh", "#!/bin/sh\n", 0o4755)], "tree.unsupported_entry"], + ["group-writable file", [file("w.md", "x\n", 0o664)], "tree.unsupported_entry"], + ] as const)("%s", async (_name, specs, code) => { + expect(await tarCode([skill, ...specs])).toBe(code); + }); + + it("rejects a tree with no files", async () => { + expect(await tarCode([{ name: enc.encode("empty/"), typeflag: "5", mode: 0o755 }])).toBe("tree.empty"); + }); +}); + +describe("tar reader fails closed on damaged archives", () => { + it("rejects a truncated archive", async () => { + const bytes = await tarBytes([skill, file("refs/a.md", new Uint8Array(4000).fill(97))]); + expect(await errorCodeOf(() => readTarTree(bytes.subarray(0, 2048)))).toBe("tree.read_failed"); + }); + + it("rejects a header with a bad checksum", async () => { + const bytes = await tarBytes([skill]); + bytes[0] = bytes[0]! ^ 0x01; + expect(await errorCodeOf(() => readTarTree(bytes))).toBe("tree.read_failed"); + }); + + it("rejects a corrupt gzip stream", async () => { + const gz = gzipSync(await tarBytes([skill])); + gz[gz.length - 6] = gz[gz.length - 6]! ^ 0xff; + expect(await errorCodeOf(() => readTarTree(gz))).toBe("tree.read_failed"); + }); + + it("accepts an archive without the end-of-archive blocks", async () => { + const manifest = await readTarTree(await tarBytes([skill], false)); + expect(manifest.files.map((f) => f.path)).toEqual(["SKILL.md"]); + }); +}); + +describe("tar reader enforces limits while streaming", () => { + it("stops reading a file once it exceeds the per-file limit", async () => { + let pulled = 0; + const source = (async function* () { + for await (const chunk of tarStream([skill, { ...file("bomb.bin"), data: undefined, fill: { size: 64 << 20, byte: 0 } }])) { + pulled += chunk.length; + yield chunk; + } + })(); + expect(await errorCodeOf(() => readTarTree(source, { limits: { maxEntries: 10, maxFiles: 10, maxFileSize: 4 << 20, maxTreeSize: 8 << 20 } }))).toBe( + "tree.limit_exceeded", + ); + expect(pulled).toBeLessThan(8 << 20); + }); + + it("stops inflating a gzip bomb at the default limit", async () => { + // 512 MiB of zeros inflates from ~0.5 MB; the reader must stop near the 32 MiB file limit. + const gz = await gzipStream(tarStream([skill, { ...file("bomb.bin"), data: undefined, fill: { size: 512 << 20, byte: 0 } }])); + let consumed = 0; + const source = (async function* () { + for (let at = 0; at < gz.length; at += 1024) { + const chunk = gz.subarray(at, at + 1024); + consumed += chunk.length; + yield chunk; + } + })(); + expect(await errorCodeOf(() => readTarTree(source))).toBe("tree.limit_exceeded"); + expect(consumed).toBeLessThan(gz.length / 2); + }, 120_000); + + it("rejects more files than the file limit", async () => { + const specs = Array.from({ length: 5 }, (_, n) => file(`f${n}.md`)); + const limits = { maxEntries: 100, maxFiles: 4, maxFileSize: 1024, maxTreeSize: 4096 }; + expect(await errorCodeOf(() => readTarTree(tarStream(specs), { limits }))).toBe("tree.limit_exceeded"); + }); +}); + +describe("one tree, one digest", () => { + const files = [ + { name: "SKILL.md", data: SKILL, mode: 0o644 }, + { name: "scripts/run.sh", data: enc.encode("#!/bin/sh\necho hi\n"), mode: 0o755 }, + { name: "refs/été.md", data: enc.encode("summer\n"), mode: 0o644 }, + { name: "refs/z.md", data: new Uint8Array(70_000).fill(122), mode: 0o644 }, + ]; + + it("tar, tar.gz, zip and presented entries agree in any order", async () => { + const specs = files.map((f) => file(f.name, f.data, f.mode)); + const tar = await tarBytes(specs); + const reversed = await tarBytes([...specs].reverse()); + const zip = zipBytes(files.map((f, i) => ({ name: enc.encode(f.name), data: f.data, unixMode: 0o100000 | f.mode, method: i % 2 === 0 ? 8 : 0 }))); + const presented = buildTree( + files.map((f) => ({ path: f.name, mode: f.mode === 0o755 ? "100755" : "100644", size: f.data.length, sha256: sha256(f.data), kind: "regular" as const })), + ); + const digests = [ + (await readTarTree(tar)).digest, + (await readTarTree(reversed)).digest, + (await readTarTree(gzipSync(tar))).digest, + (await readTarTree(new Blob([gzipSync(tar)]).stream())).digest, + (await readZipTree(zip)).digest, + presented.digest, + ]; + expect(new Set(digests).size).toBe(1); + expect(digests[0]).toMatch(/^sha256:[0-9a-f]{64}$/); + }); +}); diff --git a/packages/skill-tree/test/tar-writer.ts b/packages/skill-tree/test/tar-writer.ts new file mode 100644 index 0000000..8ec6e2a --- /dev/null +++ b/packages/skill-tree/test/tar-writer.ts @@ -0,0 +1,94 @@ +/** + * Minimal streaming ustar/PAX writer for tests. Content can be given as bytes + * or as a size and fill byte, so limit vectors stream without large buffers. + */ +export interface TarSpec { + name: Uint8Array; + typeflag: string; + mode: number; + data?: Uint8Array; + fill?: { size: number; byte: number }; + /** PAX records for an `x` header before this entry, or the records of a `g` entry. */ + pax?: Record; +} + +const enc = new TextEncoder(); + +function octal(value: number, width: number): Uint8Array { + return enc.encode(value.toString(8).padStart(width - 1, "0") + "\0"); +} + +export function paxData(records: Record): Uint8Array { + const parts: Uint8Array[] = []; + for (const [key, raw] of Object.entries(records)) { + const value = typeof raw === "string" ? enc.encode(raw) : raw; + const body = enc.encode(` ${key}=`).length + value.length + 1; + let len = body + 1; + while (String(len).length + body !== len) len = String(len).length + body; + parts.push(enc.encode(`${len} ${key}=`), value, enc.encode("\n")); + } + return concat(parts); +} + +export function concat(parts: readonly Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +export function tarHeader(name: Uint8Array, typeflag: string, mode: number, size: number): Uint8Array { + const block = new Uint8Array(512); + block.set(name.subarray(0, 100), 0); + block.set(octal(mode, 8), 100); + block.set(octal(0, 8), 108); + block.set(octal(0, 8), 116); + block.set(octal(size, 12), 124); + block.set(octal(0, 12), 136); + block[156] = typeflag.charCodeAt(0); + block.set(enc.encode("ustar\u000000"), 257); + block.fill(0x20, 148, 156); + const sum = block.reduce((n, b) => n + b, 0); + block.set(octal(sum, 7), 148); + block[155] = 0x20; + return block; +} + +const pad = (size: number) => new Uint8Array((512 - (size % 512)) % 512); + +async function* fillChunks(size: number, byte: number): AsyncGenerator { + const block = new Uint8Array(1 << 20).fill(byte); + for (let left = size; left > 0; left -= block.length) yield block.subarray(0, Math.min(left, block.length)); +} + +/** Streams a tar archive of `specs`, closed by two zero blocks unless `trailer` is false. */ +export async function* tarStream(specs: readonly TarSpec[], trailer = true): AsyncGenerator { + for (const spec of specs) { + if (spec.typeflag === "g") { + const data = paxData(spec.pax ?? {}); + yield concat([tarHeader(spec.name, "g", 0o644, data.length), data, pad(data.length)]); + continue; + } + const records: Record = { ...(spec.pax ?? {}) }; + if (spec.name.length > 100 && records.path === undefined) records.path = spec.name; + if (Object.keys(records).length > 0) { + const data = paxData(records); + yield concat([tarHeader(enc.encode("PaxHeader"), "x", 0o644, data.length), data, pad(data.length)]); + } + const size = spec.data?.length ?? spec.fill?.size ?? 0; + yield tarHeader(spec.name, spec.typeflag, spec.mode, size); + if (spec.data !== undefined) yield spec.data; + else if (spec.fill !== undefined) yield* fillChunks(spec.fill.size, spec.fill.byte); + yield pad(size); + } + if (trailer) yield new Uint8Array(1024); +} + +export async function tarBytes(specs: readonly TarSpec[], trailer = true): Promise { + const parts: Uint8Array[] = []; + for await (const part of tarStream(specs, trailer)) parts.push(part.slice()); + return concat(parts); +} diff --git a/packages/skill-tree/test/tree-vectors.test.ts b/packages/skill-tree/test/tree-vectors.test.ts new file mode 100644 index 0000000..fd8ceef --- /dev/null +++ b/packages/skill-tree/test/tree-vectors.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from "vitest"; +import { SkillTreeError, buildTree, readTarTree, readZipTree, serializeTree } from "../src/index"; +import type { TreeManifest } from "../src/index"; +import { tarStream } from "./tar-writer"; +import type { TarSpec } from "./tar-writer"; +import { zipBytes } from "./zip-writer"; +import type { ZipSpec } from "./zip-writer"; +import { fileBytes, loadTreeVectors, rawName, synthPath, vectorEntries } from "./vector-helpers"; +import type { TreeVector, VectorFile } from "./vector-helpers"; + +const doc = loadTreeVectors(); +const vectors = doc.vectors; + +const VECTOR_KEYS = new Set(["id", "description", "files", "synthesize", "archiveOnly", "expect"]); +const FILE_KEYS = new Set(["path", "rawPathBase64", "mode", "headerMode", "type", "paxRecords", "content", "contentBase64"]); +const SYNTH_KEYS = new Set(["pathPattern", "count", "size", "fill", "mode", "type"]); + +async function outcome(run: () => TreeManifest | Promise): Promise<{ digest?: string; error?: string; manifest?: TreeManifest }> { + try { + const manifest = await run(); + return { digest: manifest.digest, manifest }; + } catch (err) { + if (err instanceof SkillTreeError) return { error: err.code }; + throw err; + } +} + +function expectOutcome(got: { digest?: string; error?: string }, v: TreeVector): void { + if (v.expect.error !== undefined) expect(got.error, v.id).toBe(v.expect.error); + else expect(got, v.id).toMatchObject({ digest: v.expect.digest }); +} + +/** Archive mode of a vector entry: header modes as given, mode classes as octal, "" as an invalid mode. */ +function archiveMode(f: { mode?: string; headerMode?: string }): number { + if (f.headerMode !== undefined) return parseInt(f.headerMode, 8); + return f.mode === undefined || f.mode === "" ? 0o777 : parseInt(f.mode, 8); +} + +const TAR_TYPES: Record = { + "": "0", file: "0", directory: "5", symlink: "2", "archive-hardlink": "1", + "pax-global": "g", "tar-nul": "\0", cont: "7", fifo: "6", device: "3", +}; +const HEADER_ONLY = new Set(["1", "2", "3", "4", "5", "6"]); + +/** The vector as a tar stream, or undefined when tar cannot present it (walker-only types, NUL names). */ +function tarSpecs(v: TreeVector): TarSpec[] | undefined { + const specs: TarSpec[] = []; + for (const f of v.files ?? []) { + const typeflag = TAR_TYPES[f.type ?? ""]; + const name = rawName(f); + if (typeflag === undefined || name.includes(0) || (typeflag !== "g" && f.paxRecords?.path !== undefined)) return undefined; + const pax = f.paxRecords; + // Header-only types (links, devices, directories, FIFOs) carry no data, as Go's tar writer enforces. + const data = typeflag === "g" || HEADER_ONLY.has(typeflag) ? undefined : fileBytes(f); + specs.push({ name, typeflag, mode: archiveMode(f), data, ...(pax ? { pax } : {}) } as TarSpec); + } + for (const s of v.synthesize ?? []) { + const typeflag = TAR_TYPES[s.type ?? ""]; + if (typeflag === undefined) return undefined; + for (let n = 0; n < s.count; n++) { + specs.push({ name: new TextEncoder().encode(synthPath(s, n)), typeflag, mode: archiveMode({ mode: s.mode ?? "100644" }), fill: { size: s.size, byte: s.fill } }); + } + } + return specs; +} + +const ZIP_TYPES: Record = { "": 0o100000, file: 0o100000, directory: 0o040000 }; +const MAX_ZIP_VECTOR_BYTES = 8 << 20; + +/** Zip external mode: directories always carry the directory type bits; files keep the vector mode. */ +function zipMode(f: VectorFile, type: number): number { + const mode = archiveMode(f); + if (type === 0o040000) return type | (mode & 0o7777); + return (mode & 0o170000) === 0 ? type | mode : mode; +} + +/** The vector as a zip archive, or undefined when zip cannot present it. */ +function zipSpecs(v: TreeVector): ZipSpec[] | undefined { + const specs: ZipSpec[] = []; + for (const f of v.files ?? []) { + const type = ZIP_TYPES[f.type ?? ""]; + if (type === undefined || f.paxRecords !== undefined) return undefined; + specs.push({ name: rawName(f), data: type === 0o040000 ? new Uint8Array(0) : fileBytes(f), unixMode: zipMode(f, type), method: specs.length % 2 === 0 ? 8 : 0 }); + } + for (const s of v.synthesize ?? []) { + const type = ZIP_TYPES[s.type ?? ""]; + if (type === undefined || s.size * s.count > MAX_ZIP_VECTOR_BYTES) return undefined; + for (let n = 0; n < s.count; n++) { + specs.push({ name: new TextEncoder().encode(synthPath(s, n)), data: new Uint8Array(s.size).fill(s.fill), unixMode: zipMode({ mode: s.mode ?? "100644" }, type) }); + } + } + return specs; +} + +describe("skill-tree-v1 vectors (vendored unedited from AgentKit)", () => { + it("is the expected vector file and the harness models every field", () => { + expect(doc).toMatchObject({ schema: "agentkit.skill-tree-vectors/v1", contract: "agentkit.skill-tree/v1", unicode: "15.0.0" }); + expect(vectors.length).toBeGreaterThan(0); + for (const v of vectors) { + for (const key of Object.keys(v)) expect(VECTOR_KEYS.has(key), `${v.id}.${key}`).toBe(true); + for (const f of v.files ?? []) for (const key of Object.keys(f)) expect(FILE_KEYS.has(key), `${v.id} file.${key}`).toBe(true); + for (const s of v.synthesize ?? []) for (const key of Object.keys(s)) expect(SYNTH_KEYS.has(key), `${v.id} synth.${key}`).toBe(true); + expect(Object.keys(v.expect).every((k) => ["digest", "serialization", "error"].includes(k)), v.id).toBe(true); + } + }); + + it.each(vectors.map((v) => [v.id, v] as const))("buildTree: %s", async (_id, v) => { + const got = await outcome(() => buildTree(vectorEntries(v))); + expectOutcome(got, v); + if (got.manifest !== undefined) { + if (v.expect.serialization !== undefined) expect(serializeTree(got.manifest.files)).toBe(v.expect.serialization); + expect(got.manifest).toMatchObject({ schema: "agentkit.skill-tree/v1", modeSource: "recorded" }); + } + }); + + it.each(vectors.map((v) => [v.id, v] as const))("tar reader agrees: %s", async (_id, v) => { + const specs = tarSpecs(v); + if (specs === undefined) return; + expectOutcome(await outcome(() => readTarTree(tarStream(specs))), v); + }, 120_000); + + it.each(vectors.map((v) => [v.id, v] as const))("zip reader agrees: %s", async (_id, v) => { + const specs = zipSpecs(v); + if (specs === undefined) return; + expectOutcome(await outcome(() => readZipTree(zipBytes(specs))), v); + }, 60_000); +}); diff --git a/packages/skill-tree/test/vector-helpers.ts b/packages/skill-tree/test/vector-helpers.ts new file mode 100644 index 0000000..e7dde23 --- /dev/null +++ b/packages/skill-tree/test/vector-helpers.ts @@ -0,0 +1,162 @@ +/** + * Loads the vendored AgentKit conformance vectors and turns tree vectors into + * presented entries the same way the AgentKit Go harness does. + */ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { decodeName } from "../src/entry-collector"; +import { SkillTreeError } from "../src/errors"; +import { tarEntryKind } from "../src/tar-reader"; +import { headerMode } from "../src/tree"; +import type { EntryKind, TreeEntry } from "../src/tree"; + +export function fixtureBytes(relative: string): Buffer { + return readFileSync(fileURLToPath(new URL(`../fixtures/${relative}`, import.meta.url))); +} + +export function sha256(data: Uint8Array | string): string { + return createHash("sha256").update(data).digest("hex"); +} + +/** The contract code `run` fails with, or null when it succeeds. Other errors propagate. */ +export async function errorCodeOf(run: () => unknown): Promise { + try { + await run(); + return null; + } catch (err) { + if (err instanceof SkillTreeError) return err.code; + throw err; + } +} + +export interface VectorFile { + path?: string; + rawPathBase64?: string; + mode?: string; + headerMode?: string; + type?: string; + paxRecords?: Record; + content?: string; + contentBase64?: string; +} + +export interface VectorSynth { + pathPattern: string; + count: number; + size: number; + fill: number; + mode?: string; + type?: string; +} + +export interface TreeVector { + id: string; + description: string; + files?: VectorFile[]; + synthesize?: VectorSynth[]; + archiveOnly?: boolean; + expect: { digest?: string; serialization?: string; error?: string }; +} + +export interface TreeVectorFile { + schema: string; + contract: string; + spec: string; + unicode: string; + notes: string[]; + vectors: TreeVector[]; +} + +export function loadTreeVectors(): TreeVectorFile { + return JSON.parse(fixtureBytes("vectors/skill-tree-v1-vectors.json").toString("utf8")) as TreeVectorFile; +} + +/** Entry types as the AgentKit harness maps them; walker-only types read as files or specials. */ +const VECTOR_KINDS: Record = { + "": "regular", + file: "regular", + "fs-hardlink": "regular", + "reparse-other": "regular", + symlink: "symlink", + "reparse-name-surrogate": "symlink", + "archive-hardlink": "hardlink", + "reparse-af-unix": "special", + "reparse-wsl": "special", + device: "special", + fifo: "special", + socket: "special", + directory: "directory", + "pax-global": "metadata", + cont: "special", +}; + +export function vectorKind(type: string | undefined, path: string): EntryKind { + if (type === "tar-nul") return tarEntryKind("\0", path); + const kind = VECTOR_KINDS[type ?? ""]; + if (kind === undefined) throw new Error(`vector entry ${path} has unknown type ${type}`); + return kind; +} + +export function rawName(f: VectorFile): Uint8Array { + if (f.rawPathBase64 === undefined) return new TextEncoder().encode(f.path ?? ""); + if ((f.path ?? "") !== "") throw new Error(`vector entry ${f.path} sets both path and rawPathBase64`); + return Buffer.from(f.rawPathBase64, "base64"); +} + +export function fileBytes(f: VectorFile): Uint8Array { + if (f.contentBase64 !== undefined) return Buffer.from(f.contentBase64, "base64"); + if (f.content !== undefined) return new TextEncoder().encode(f.content); + throw new Error(`vector file ${f.path} has no content`); +} + +/** The entry mode: a mode string as given, or a header mode in octal mapped to its class. */ +export function vectorMode(f: VectorFile): string { + if (f.headerMode === undefined) return f.mode ?? ""; + if ((f.mode ?? "") !== "") throw new Error(`vector entry ${f.path} sets both mode and headerMode`); + return headerMode(parseInt(f.headerMode, 8)); +} + +export function synthPath(s: VectorSynth, n: number): string { + return s.pathPattern.replaceAll("{n}", String(n).padStart(5, "0")); +} + +const fillDigests = new Map(); + +/** sha256 of `size` bytes of value `fill`, without holding them in memory. */ +export function fillDigest(size: number, fill: number): string { + const key = `${size}/${fill}`; + const cached = fillDigests.get(key); + if (cached !== undefined) return cached; + const hash = createHash("sha256"); + const block = Buffer.alloc(64 * 1024, fill); + for (let left = size; left > 0; left -= block.length) hash.update(block.subarray(0, Math.min(left, block.length))); + const digest = hash.digest("hex"); + fillDigests.set(key, digest); + return digest; +} + +export function vectorEntries(v: TreeVector): TreeEntry[] { + const entries: TreeEntry[] = []; + for (const f of v.files ?? []) { + const name = decodeName(rawName(f)); + const data = fileBytes(f); + entries.push({ + path: name.path, + utf8Valid: name.utf8Valid, + mode: vectorMode(f), + size: data.length, + sha256: sha256(data), + kind: vectorKind(f.type, name.path), + ...(f.paxRecords === undefined ? {} : { paxRecords: f.paxRecords }), + }); + } + for (const s of v.synthesize ?? []) { + const kind = vectorKind(s.type, s.pathPattern); + const digest = fillDigest(s.size, s.fill); + for (let n = 0; n < s.count; n++) { + entries.push({ path: synthPath(s, n), mode: s.mode ?? "100644", size: s.size, sha256: digest, kind }); + } + } + return entries; +} diff --git a/packages/skill-tree/test/zip-reader.test.ts b/packages/skill-tree/test/zip-reader.test.ts new file mode 100644 index 0000000..d626f60 --- /dev/null +++ b/packages/skill-tree/test/zip-reader.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; +import { readZipTree } from "../src/index"; +import type { RandomAccessSource } from "../src/index"; +import { zipBytes } from "./zip-writer"; +import type { ZipOptions, ZipSpec } from "./zip-writer"; +import { errorCodeOf } from "./vector-helpers"; + +const enc = new TextEncoder(); + +function entry(name: string, data = "x\n", extra: Partial = {}): ZipSpec { + return { name: enc.encode(name), data: enc.encode(data), unixMode: 0o100644, ...extra }; +} + +const skill = entry("SKILL.md", "---\nname: demo\ndescription: Demo.\n---\n", { method: 8 }); +const baseline = await readZipTree(zipBytes([skill, entry("refs/a.md", "a\n".repeat(500), { method: 8 })])); + +function zipCode(specs: ZipSpec[], options?: ZipOptions): Promise { + return errorCodeOf(() => readZipTree(zipBytes(specs, options))); +} + +describe("zip reader accepts every valid layout", () => { + it.each([12, 16, 20, 24] as const)("data descriptor of %i bytes", async (descriptor) => { + const got = await readZipTree(zipBytes([skill, entry("refs/a.md", "a\n".repeat(500), { method: 8, descriptor })])); + expect(got.digest).toBe(baseline.digest); + }); + + it("stored and deflated entries hash the same content", async () => { + const got = await readZipTree(zipBytes([{ ...skill, method: 0 }, entry("refs/a.md", "a\n".repeat(500))])); + expect(got.digest).toBe(baseline.digest); + }); + + it("reads through a random-access source and a Blob", async () => { + const bytes = zipBytes([skill, entry("refs/a.md", "a\n".repeat(500), { method: 8 })]); + const source: RandomAccessSource = { size: bytes.length, read: async (offset, length) => bytes.slice(offset, offset + length) }; + expect((await readZipTree(source)).digest).toBe(baseline.digest); + expect((await readZipTree(new Blob([new Uint8Array(bytes)]))).digest).toBe(baseline.digest); + }); + + it("records executable files", async () => { + const got = await readZipTree(zipBytes([skill, entry("run.sh", "#!/bin/sh\n", { unixMode: 0o100755 })])); + expect(got.files.find((f) => f.path === "run.sh")?.mode).toBe("100755"); + }); +}); + +describe("zip reader rejects unsafe or unsupported entries", () => { + it.each([ + ["zip-slip", [entry("../evil.md")], "tree.unsafe_path"], + ["absolute path", [entry("/evil.md")], "tree.unsafe_path"], + ["case collision", [entry("Refs/a.md"), entry("refs/b.md")], "tree.case_collision"], + ["symbolic link", [entry("link", "/etc", { unixMode: 0o120777 })], "tree.unsupported_entry"], + ["setuid file", [entry("run.sh", "x", { unixMode: 0o104755 })], "tree.unsupported_entry"], + ["non-Unix host attributes", [entry("a.md", "x", { madeByHost: 0 })], "tree.unsupported_entry"], + ["encrypted entry", [entry("a.md", "x", { flags: 0x1 })], "tree.unsupported_entry"], + ["central CRC disagrees with the local header", [entry("a.md", "x", { centralCrc: 0x12345678 })], "tree.unsupported_entry"], + ] as const)("%s", async (_name, specs, code) => { + expect(await zipCode([skill, ...specs])).toBe(code); + }); + + it("rejects an entry whose data does not match its CRC", async () => { + const bytes = zipBytes([skill, entry("a.md", "hello\n")]); + const at = Buffer.from(bytes).indexOf("hello\n"); + bytes[at] = 0x48; + expect(await errorCodeOf(() => readZipTree(bytes))).toBe("tree.read_failed"); + }); + + it("rejects deflate data that is corrupt", async () => { + const bytes = zipBytes([skill, entry("a.md", "z".repeat(4096), { method: 8 })]); + const local = Buffer.from(bytes).indexOf("a.md") + 4; + bytes.fill(0xff, local, local + 6); + expect(await errorCodeOf(() => readZipTree(bytes))).toBe("tree.read_failed"); + }); +}); + +describe("zip reader rejects archive-level ambiguity", () => { + it("rejects bytes before the first local entry", async () => { + expect(await zipCode([skill], { prefix: enc.encode("#!/bin/sh\nexit 0\n") })).toBe("tree.unsupported_entry"); + }); + + it("rejects bytes between the entries and the central directory", async () => { + expect(await zipCode([skill], { gap: new Uint8Array(32) })).toBe("tree.unsupported_entry"); + }); + + it("rejects truncated and non-zip input", async () => { + const bytes = zipBytes([skill]); + expect(await errorCodeOf(() => readZipTree(bytes.subarray(0, bytes.length - 10)))).toBe("tree.read_failed"); + expect(await errorCodeOf(() => readZipTree(enc.encode("not a zip archive")))).toBe("tree.read_failed"); + expect(await errorCodeOf(() => readZipTree(new Uint8Array(0)))).toBe("tree.read_failed"); + }); + + it("enforces the per-file limit on inflated size", async () => { + const limits = { maxEntries: 10, maxFiles: 10, maxFileSize: 1024, maxTreeSize: 4096 }; + const bytes = zipBytes([skill, entry("big.bin", "0".repeat(1 << 20), { method: 8 })]); + expect(await errorCodeOf(() => readZipTree(bytes, { limits }))).toBe("tree.limit_exceeded"); + }); +}); diff --git a/packages/skill-tree/test/zip-writer.ts b/packages/skill-tree/test/zip-writer.ts new file mode 100644 index 0000000..b0b2271 --- /dev/null +++ b/packages/skill-tree/test/zip-writer.ts @@ -0,0 +1,78 @@ +/** + * Minimal zip writer for tests, with knobs for the layouts the reader must + * accept (stored, deflate, data descriptors with and without signature, + * zip64 descriptors) and the defects it must reject. + */ +import { deflateRawSync } from "node:zlib"; +import { crc32 } from "../src/zip-records"; +import { concat } from "./tar-writer"; + +export interface ZipSpec { + name: Uint8Array; + data: Uint8Array; + /** Unix mode including type bits, e.g. 0o100644 or 0o040755. */ + unixMode: number; + method?: 0 | 8; + /** Data descriptor layout; omitted means no descriptor (flag bit 3 clear). */ + descriptor?: 12 | 16 | 20 | 24; + /** Overrides written into the central directory only. */ + centralCrc?: number; + flags?: number; + madeByHost?: number; +} + +export interface ZipOptions { + prefix?: Uint8Array; + /** Bytes inserted between the last entry and the central directory. */ + gap?: Uint8Array; +} + +function le(width: 2 | 4 | 8, value: number): Uint8Array { + const out = new Uint8Array(width); + const view = new DataView(out.buffer); + if (width === 2) view.setUint16(0, value, true); + else if (width === 4) view.setUint32(0, value, true); + else view.setBigUint64(0, BigInt(value), true); + return out; +} + +function descriptorBytes(kind: 12 | 16 | 20 | 24, crc: number, csize: number, usize: number): Uint8Array { + const sig = kind === 16 || kind === 24 ? [le(4, 0x08074b50)] : []; + const sizes = kind === 20 || kind === 24 ? [le(8, csize), le(8, usize)] : [le(4, csize), le(4, usize)]; + return concat([...sig, le(4, crc), ...sizes]); +} + +export function zipBytes(specs: readonly ZipSpec[], options: ZipOptions = {}): Uint8Array { + const locals: Uint8Array[] = []; + const centrals: Uint8Array[] = []; + let offset = options.prefix?.length ?? 0; + for (const spec of specs) { + const method = spec.method ?? 0; + const body = method === 8 ? new Uint8Array(deflateRawSync(spec.data)) : spec.data; + const crc = crc32(spec.data); + const flags = (spec.flags ?? 0) | (spec.descriptor !== undefined ? 0x8 : 0) | 0x800; + const zeroInLocal = spec.descriptor !== undefined; + const local = concat([ + le(4, 0x04034b50), le(2, 20), le(2, flags), le(2, method), le(2, 0), le(2, 0x21), + le(4, zeroInLocal ? 0 : crc), le(4, zeroInLocal ? 0 : body.length), le(4, zeroInLocal ? 0 : spec.data.length), + le(2, spec.name.length), le(2, 0), spec.name, body, + spec.descriptor !== undefined ? descriptorBytes(spec.descriptor, crc, body.length, spec.data.length) : new Uint8Array(0), + ]); + centrals.push(concat([ + le(4, 0x02014b50), le(2, ((spec.madeByHost ?? 3) << 8) | 20), le(2, 20), le(2, flags), le(2, method), + le(2, 0), le(2, 0x21), le(4, spec.centralCrc ?? crc), le(4, body.length), le(4, spec.data.length), + le(2, spec.name.length), le(2, 0), le(2, 0), le(2, 0), le(2, 0), le(4, (spec.unixMode << 16) >>> 0), + le(4, offset), spec.name, + ])); + locals.push(local); + offset += local.length; + } + const gap = options.gap ?? new Uint8Array(0); + const cd = concat(centrals); + const cdOffset = offset + gap.length; + const end = concat([ + le(4, 0x06054b50), le(2, 0), le(2, 0), le(2, specs.length), le(2, specs.length), + le(4, cd.length), le(4, cdOffset), le(2, 0), + ]); + return concat([options.prefix ?? new Uint8Array(0), ...locals, gap, cd, end]); +} diff --git a/packages/skill-tree/tsconfig.json b/packages/skill-tree/tsconfig.json new file mode 100644 index 0000000..d1e852f --- /dev/null +++ b/packages/skill-tree/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "types": ["node"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true + }, + "include": ["src/**/*", "test/**/*"] +} diff --git a/packages/skill-tree/vitest.config.ts b/packages/skill-tree/vitest.config.ts new file mode 100644 index 0000000..f082446 --- /dev/null +++ b/packages/skill-tree/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +/** Lets `pnpm --filter @skillx/skill-tree test` run the package suite on its own; the root config also includes it. */ +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc3528c..bf18901 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: devDependencies: vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + version: 4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) apps/web: dependencies: @@ -22,7 +22,7 @@ importers: version: link:../../packages/skill-identity better-auth: specifier: ^1.2.0 - version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@cloudflare/workers-types@4.20260210.0)(gel@2.2.0)(kysely@0.28.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@cloudflare/workers-types@4.20260210.0)(gel@2.2.0)(kysely@0.28.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1)) drizzle-orm: specifier: ^0.41.0 version: 0.41.0(@cloudflare/workers-types@4.20260210.0)(gel@2.2.0)(kysely@0.28.11) @@ -47,16 +47,16 @@ importers: devDependencies: '@cloudflare/vite-plugin': specifier: ^1.13.5 - version: 1.23.1(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260210.0)) + version: 1.23.1(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260210.0)) '@cloudflare/workers-types': specifier: ^4.20250204.0 version: 4.20260210.0 '@react-router/dev': specifier: ^7.10.0 - version: 7.13.0(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2))(wrangler@4.63.0(@cloudflare/workers-types@4.20260210.0)) + version: 7.13.0(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1))(wrangler@4.63.0(@cloudflare/workers-types@4.20260210.0))(yaml@2.9.1) '@tailwindcss/vite': specifier: ^4.1.13 - version: 4.1.18(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 4.1.18(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1)) '@types/node': specifier: ^22.19.10 version: 22.19.10 @@ -77,10 +77,10 @@ importers: version: 5.9.3 vite: specifier: ^7.1.7 - version: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + version: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1)) wrangler: specifier: ^4.63.0 version: 4.63.0(@cloudflare/workers-types@4.20260210.0) @@ -105,7 +105,7 @@ importers: version: 22.19.10 tsup: specifier: ^8.0.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.9.1) typescript: specifier: ^5.6.0 version: 5.9.3 @@ -118,6 +118,19 @@ importers: packages/skill-identity: {} + packages/skill-tree: + dependencies: + yaml: + specifier: ^2.9.1 + version: 2.9.1 + devDependencies: + '@types/node': + specifier: ^22.19.10 + version: 22.19.10 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + packages: '@babel/code-frame@7.29.0': @@ -2716,6 +2729,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} @@ -2945,12 +2963,12 @@ snapshots: optionalDependencies: workerd: 1.20260205.0 - '@cloudflare/vite-plugin@1.23.1(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260210.0))': + '@cloudflare/vite-plugin@1.23.1(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260210.0))': dependencies: '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0) miniflare: 4.20260205.0 unenv: 2.0.0-rc.24 - vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) wrangler: 4.63.0(@cloudflare/workers-types@4.20260210.0) ws: 8.18.0 transitivePeerDependencies: @@ -3437,7 +3455,7 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@react-router/dev@7.13.0(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2))(wrangler@4.63.0(@cloudflare/workers-types@4.20260210.0))': + '@react-router/dev@7.13.0(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1))(wrangler@4.63.0(@cloudflare/workers-types@4.20260210.0))(yaml@2.9.1)': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -3467,8 +3485,8 @@ snapshots: semver: 7.7.4 tinyglobby: 0.2.15 valibot: 1.2.0(typescript@5.9.3) - vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) - vite-node: 3.2.4(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) + vite-node: 3.2.4(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) optionalDependencies: typescript: 5.9.3 wrangler: 4.63.0(@cloudflare/workers-types@4.20260210.0) @@ -3638,12 +3656,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) '@types/chai@5.2.3': dependencies: @@ -3699,13 +3717,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) '@vitest/pretty-format@4.0.18': dependencies: @@ -3768,7 +3786,7 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@cloudflare/workers-types@4.20260210.0)(gel@2.2.0)(kysely@0.28.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)): + better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@cloudflare/workers-types@4.20260210.0)(gel@2.2.0)(kysely@0.28.11))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1)): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -3787,7 +3805,7 @@ snapshots: drizzle-orm: 0.41.0(@cloudflare/workers-types@4.20260210.0)(gel@2.2.0)(kysely@0.28.11) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - vitest: 4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + vitest: 4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) better-call@1.1.8(zod@4.3.6): dependencies: @@ -4568,12 +4586,13 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.9.1): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.6 + yaml: 2.9.1 postcss@8.5.6: dependencies: @@ -4829,7 +4848,7 @@ snapshots: tslib@2.8.1: optional: true - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.9.1): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -4840,7 +4859,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.9.1) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -4926,13 +4945,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2): + vite-node@3.2.4(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4947,18 +4966,18 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2): + vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -4971,11 +4990,12 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 + yaml: 2.9.1 - vitest@4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2): + vitest@4.0.18(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -4992,7 +5012,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.9.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.10 @@ -5050,6 +5070,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.9.1: {} + youch-core@0.3.3: dependencies: '@poppinss/exception': 1.2.3 diff --git a/vitest.config.ts b/vitest.config.ts index 5f0154b..29f07f7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -24,6 +24,7 @@ export default defineConfig({ "packages/cli/src/**/*.test.ts", "packages/contracts/src/**/*.test.ts", "packages/skill-identity/**/*.test.ts", + "packages/skill-tree/test/**/*.test.ts", ], }, }); From 8ef77690e027f0d17cec15ce0dd54546028d9001 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 10:18:29 +0700 Subject: [PATCH 02/14] ci(skill-tree): typecheck and test the skill tree package on pull requests The repository has no pull request CI yet, so run the package typecheck and its conformance suite when the package, the lockfile or the test config change. Refs #54 --- .github/workflows/skill-tree-ci.yml | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/skill-tree-ci.yml diff --git a/.github/workflows/skill-tree-ci.yml b/.github/workflows/skill-tree-ci.yml new file mode 100644 index 0000000..dc3d10d --- /dev/null +++ b/.github/workflows/skill-tree-ci.yml @@ -0,0 +1,46 @@ +name: Skill tree CI + +on: + pull_request: + paths: + - "packages/skill-tree/**" + - "pnpm-lock.yaml" + - "vitest.config.ts" + - ".github/workflows/skill-tree-ci.yml" + push: + branches: [main] + paths: + - "packages/skill-tree/**" + - "pnpm-lock.yaml" + - "vitest.config.ts" + - ".github/workflows/skill-tree-ci.yml" + +permissions: + contents: read + +concurrency: + group: skill-tree-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm --filter @skillx/skill-tree typecheck + + - name: Test (AgentKit conformance vectors and negative cases) + run: pnpm exec vitest run packages/skill-tree/test From 95dfb49a20c99ec83ab9eaaeef4c5e9bcd942565 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 10:52:28 +0700 Subject: [PATCH 03/14] fix(skill-tree): align tar, zip and NFC handling with the Go reference - NFC/NFD follow x/text stream-safe processing: a run of more than 30 non-starters gets a CGJ, so such a path is non-NFC; composition ports reorderBuffer.compose; counts and differential vectors come from Go - header modes outside a safe, non-negative 7-digit octal value are rejected before the type bits are cleared - zip entries with header faults or non-regular types are unsupported without inflating their data or counting toward file and byte limits - tar reads stay within a total byte budget covering non-regular data and PAX/GNU extension chains - GNU sparse files apply GNU.sparse.name, size and realsize and validate their maps as Go does; malformed maps fail with tree.read_failed - numeric and PAX parsing match Go: NUL-cut octal, negative base-256, signed and zero-padded PAX lengths, PAX times, all header numerics - an empty GNU long name clears an earlier one; PAX records live in a prototype-free object; non-safe sizes fail the limit step - collision keys fold each path once; vector harness asserts reader coverage --- packages/skill-tree/.gitattributes | 1 + packages/skill-tree/package.json | 3 + .../skill-tree/scripts/dump-go-unicode.go | 133 +++++++++ .../skill-tree/scripts/gen-unicode-tables.mjs | 3 +- .../stream-safe-15.0.0-go-vectors.json | 1 + .../skill-tree/scripts/unicode-15.0.0-go.json | 2 +- packages/skill-tree/src/byte-source.ts | 13 +- packages/skill-tree/src/collision.ts | 18 +- packages/skill-tree/src/entry-collector.ts | 35 ++- packages/skill-tree/src/tar-header.ts | 252 +++++++++++++----- packages/skill-tree/src/tar-reader.ts | 91 +++++-- packages/skill-tree/src/tar-sparse.ts | 167 ++++++++++++ packages/skill-tree/src/tree.ts | 28 +- packages/skill-tree/src/unicode-15-tables.ts | 3 +- packages/skill-tree/src/unicode-15.ts | 140 +++++++--- packages/skill-tree/src/zip-reader.ts | 18 +- packages/skill-tree/test/path-fuzz.test.ts | 14 + packages/skill-tree/test/provenance.test.ts | 47 +++- packages/skill-tree/test/tar-reader.test.ts | 215 ++++++++++++++- packages/skill-tree/test/tree-rules.test.ts | 42 +++ packages/skill-tree/test/tree-vectors.test.ts | 29 +- packages/skill-tree/test/zip-reader.test.ts | 19 ++ 22 files changed, 1100 insertions(+), 174 deletions(-) create mode 100644 packages/skill-tree/scripts/stream-safe-15.0.0-go-vectors.json create mode 100644 packages/skill-tree/src/tar-sparse.ts create mode 100644 packages/skill-tree/test/tree-rules.test.ts diff --git a/packages/skill-tree/.gitattributes b/packages/skill-tree/.gitattributes index 35fee4c..9bb8071 100644 --- a/packages/skill-tree/.gitattributes +++ b/packages/skill-tree/.gitattributes @@ -1,3 +1,4 @@ # Pinned inputs are hashed byte for byte; never convert their line endings. fixtures/** -text scripts/unicode-15.0.0-go.json -text +scripts/stream-safe-15.0.0-go-vectors.json -text diff --git a/packages/skill-tree/package.json b/packages/skill-tree/package.json index 607a537..4fc376b 100644 --- a/packages/skill-tree/package.json +++ b/packages/skill-tree/package.json @@ -8,6 +8,9 @@ ".": "./src/index.ts" }, "types": "./src/index.ts", + "engines": { + "node": ">=21.2" + }, "scripts": { "typecheck": "tsc --noEmit", "test": "vitest run", diff --git a/packages/skill-tree/scripts/dump-go-unicode.go b/packages/skill-tree/scripts/dump-go-unicode.go index 9a51c56..aa1df8f 100644 --- a/packages/skill-tree/scripts/dump-go-unicode.go +++ b/packages/skill-tree/scripts/dump-go-unicode.go @@ -10,13 +10,20 @@ // Run it from the AgentKit apps/cli module, which pins x/text: // // cd /apps/cli && go run /packages/skill-tree/scripts/dump-go-unicode.go > /packages/skill-tree/scripts/unicode-15.0.0-go.json +// +// With -vectors it writes the stream-safe normalization vectors instead +// (scripts/stream-safe-15.0.0-go-vectors.json). package main import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" + "math/rand" "os" "runtime" + "strings" "unicode" "unicode/utf8" @@ -34,6 +41,40 @@ type dump struct { // one-step pair for a primary composite, the full decomposition otherwise. Decompositions [][]rune `json:"decompositions"` Composites []rune `json:"composites"` + // StreamSafe lists [code point, leading, trailing] non-starter counts + // that x/text norm uses for Stream-Safe Text Processing: a run of more + // than 30 non-starters gets a U+034F CGJ inserted, so a path holding one + // is not NFC. The counts are unexported, so they are measured. + StreamSafe [][3]rune `json:"streamSafe"` +} + +const ( + cgj = "͏" + probeMark = "̅" // a non-starter that never composes + maxNonStarters = 30 +) + +// overflows reports whether NFD inserts a CGJ into s. +func overflows(s string) bool { + return strings.Count(norm.NFD.String(s), cgj) > strings.Count(s, cgj) +} + +// nonStarters measures how many non-starters r adds before (lead) or after +// (trail) itself: k marks plus r overflow exactly when k+count > 30. +func nonStarters(r rune, lead bool) rune { + count := rune(0) + for k := maxNonStarters; k >= maxNonStarters-3; k-- { + marks := strings.Repeat(probeMark, k) + s := string(r) + marks + if lead { + s = marks + string(r) + } + if !overflows(s) { + break + } + count = rune(maxNonStarters + 1 - k) + } + return count } func addRange(ranges [][2]rune, r rune) [][2]rune { @@ -62,7 +103,96 @@ func compositePair(r rune, full []rune) ([2]rune, bool) { return [2]rune{}, false } +// vectorAlphabet mixes starters, starters with trailing non-starters, marks of +// several classes, multi-mark decompositions, Jamo and Indic vowels that +// combine backwards, a CGJ and "/". Vector inputs are letters indexing it. +var vectorAlphabet = []rune{ + 'a', 'e', '/', 0x00E1, 0x01D6, 0x034F, 0x0B47, 0x1100, 0xAC00, 0xAC01, // starters + 0x0300, 0x0301, 0x0305, 0x0316, 0x0344, 0x0345, 0x0F73, 0x3099, // non-starters + 0x0B3E, 0x1161, 0x11A8, 0xFF9E, // combine backwards or compatibility marks +} + +const vectorStarters = 10 + +type streamSafeVector struct { + In string `json:"in"` + IsNFC bool `json:"isNfc"` + NFD string `json:"nfd"` + NFC string `json:"nfc"` + Strict bool `json:"nfcEqual"` +} + +func shortHash(s string) string { + sum := sha256.Sum256([]byte(s)) + return hex.EncodeToString(sum[:8]) +} + +// writeVectors prints differential vectors for the stream-safe NFC and NFD +// behaviour: deterministic inputs with long non-starter runs, each with +// norm.NFC.IsNormalString and short hashes of NFD.String and NFC.String. +func writeVectors() { + rng := rand.New(rand.NewSource(15)) + letter := func(i int) byte { return "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[i] } + var out struct { + Alphabet []rune `json:"alphabet"` + Cases []streamSafeVector `json:"cases"` + } + out.Alphabet = vectorAlphabet + for n := 0; n < 800; n++ { + var in []byte + if n%2 == 1 { + // Canonically ordered runs (U+0316 then U+0305) around the 30 limit, + // so the NFC verdict turns on the run length alone. + for seg := rng.Intn(2) + 1; seg > 0; seg-- { + in = append(in, letter(rng.Intn(vectorStarters))) + low := rng.Intn(4) + for k := 26 + rng.Intn(9); k > 0; k-- { + if low > 0 { + in = append(in, letter(13)) // U+0316 + low-- + } else { + in = append(in, letter(12)) // U+0305 + } + } + } + } + for seg := rng.Intn(3) + 1; n%2 == 0 && seg > 0; seg-- { + for k := rng.Intn(3); k > 0; k-- { + in = append(in, letter(rng.Intn(len(vectorAlphabet)))) + } + for k := 24 + rng.Intn(14); k > 0; k-- { + i := vectorStarters + rng.Intn(len(vectorAlphabet)-vectorStarters) + if rng.Intn(12) == 0 { + i = rng.Intn(len(vectorAlphabet)) + } + in = append(in, letter(i)) + } + } + var s strings.Builder + for _, c := range in { + s.WriteRune(vectorAlphabet[c-'A']) + } + str := s.String() + out.Cases = append(out.Cases, streamSafeVector{ + In: string(in), + IsNFC: norm.NFC.IsNormalString(str), + NFD: shortHash(norm.NFD.String(str)), + NFC: shortHash(norm.NFC.String(str)), + Strict: norm.NFC.String(str) == str, + }) + } + enc := json.NewEncoder(os.Stdout) + if err := enc.Encode(out); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + func main() { + if len(os.Args) > 1 && os.Args[1] == "-vectors" { + writeVectors() + return + } d := dump{UnicodeVersion: unicode.Version, NormVersion: norm.Version, GoVersion: runtime.Version()} for r := rune(0); r <= unicode.MaxRune; r++ { if unicode.In(r, unicode.L, unicode.M, unicode.N, unicode.P, unicode.S, unicode.Z, unicode.Cc, unicode.Cf, unicode.Co, unicode.Cs) { @@ -79,6 +209,9 @@ func main() { if ccc := props.CCC(); ccc != 0 { d.CombiningClass = append(d.CombiningClass, [2]rune{r, rune(ccc)}) } + if lead, trail := nonStarters(r, true), nonStarters(r, false); lead != 0 || trail != 0 { + d.StreamSafe = append(d.StreamSafe, [3]rune{r, lead, trail}) + } if r >= 0xAC00 && r <= 0xD7A3 { continue // Hangul syllables are algorithmic. } diff --git a/packages/skill-tree/scripts/gen-unicode-tables.mjs b/packages/skill-tree/scripts/gen-unicode-tables.mjs index 16fbe60..d189407 100644 --- a/packages/skill-tree/scripts/gen-unicode-tables.mjs +++ b/packages/skill-tree/scripts/gen-unicode-tables.mjs @@ -28,7 +28,7 @@ const INPUTS = { }, goTables: { path: "scripts/unicode-15.0.0-go.json", - sha256: "baa335155dcbaf0cfe8f40fcc0a98232c4134e821a08c3f198f829a51d911ad5", + sha256: "0d0155948ceea434fa241b59937db1a41689fa2ccb8231d35d96a2c56f57b52b", }, }; @@ -123,6 +123,7 @@ export const COMBINING_CLASSES = "${encodeValueRanges(tables.combiningClass)}"; export const CANONICAL_DECOMPOSITIONS = "${encodeMap(tables.decompositions.map(([cp, ...m]) => [cp, m]))}"; export const PRIMARY_COMPOSITES = "${encodeList(tables.composites)}"; export const CASE_FOLDING = "${encodeMap(foldEntries)}"; +export const STREAM_SAFE = "${encodeMap(tables.streamSafe.map(([cp, lead, trail]) => [cp, [lead, trail]]))}"; `; const target = join(root, "src", "unicode-15-tables.ts"); diff --git a/packages/skill-tree/scripts/stream-safe-15.0.0-go-vectors.json b/packages/skill-tree/scripts/stream-safe-15.0.0-go-vectors.json new file mode 100644 index 0000000..19917f6 --- /dev/null +++ b/packages/skill-tree/scripts/stream-safe-15.0.0-go-vectors.json @@ -0,0 +1 @@ +{"alphabet":[97,101,47,225,470,847,2887,4352,44032,44033,768,769,773,790,836,837,3955,12441,2878,4449,4520,65438],"cases":[{"in":"MPSUVKNKRSLLMRNPSPMMSVRKNKTOUQ","isNfc":false,"nfd":"fcd828103302c9d1","nfc":"fcd828103302c9d1","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"32901240080e1f4b","nfc":"32901240080e1f4b","nfcEqual":false},{"in":"PQSQUMQSNOSQORHRUTLPVRMMQKMPONMOLVULPNKKNSORUONRUTRQQTVOLONTTMRKPOKSPRRUONOQUVOSRKVUUUQSPRORMQUHLNR","isNfc":false,"nfd":"bbc926e26f50dc17","nfc":"bbc926e26f50dc17","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"fafdf7784a3ad038","nfc":"331129e452f8a0df","nfcEqual":false},{"in":"VMUVSSKQKPQVVMQSLRMMSMTTVRTVUPVSKSKTRNUMNQLOROQUSSQONMRUTVPS","isNfc":false,"nfd":"3098e9677c4daaf6","nfc":"3098e9677c4daaf6","nfcEqual":false},{"in":"HMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMINNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"cb65e755c52053f8","nfc":"211a1ef9c54360a3","nfcEqual":false},{"in":"SNOSRPQVLQTVURSLRPRMPGLVTQRVRUOVKTVMUPLSPNKMLLVMHRVTOLUUVKV","isNfc":false,"nfd":"00ce451cdbbfeb00","nfc":"00ce451cdbbfeb00","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMGNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b8d7c20e519046a3","nfc":"97f0596df770d0b2","nfcEqual":true},{"in":"DAPTJKMPARTMEJTSOPJLTPKLSVRODROMFQOKNKRRLLENUPVVPRRNONKBSNPSSOQLUK","isNfc":false,"nfd":"6ec037d08af549c5","nfc":"d6e8281bbb9300fc","nfcEqual":false},{"in":"CNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMFNNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"01a92892fea716cd","nfc":"01a92892fea716cd","nfcEqual":false},{"in":"UEPTPNNUMUQSOVNVOPLVMNTKLOSVLKTOOVQQLVL","isNfc":false,"nfd":"5c38d98dd6e73f3e","nfc":"54bace6ac706627f","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"cc6286300846fff6","nfc":"734623d4e0197f4e","nfcEqual":true},{"in":"APMNSKNRRKLQLKMPQMKVRMMTHPRLFRPNQMKPNTLNONPGPRKVBUPKMSSURLSUTKTPJUMKLSPOGQUVQQRQSTKMULTHLPRSNVRJVRKPTMVNPVRE","isNfc":false,"nfd":"4e2b4a223b54039c","nfc":"97fa0a895c02bf1e","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"504cd1cd0cc5d6c4","nfc":"504cd1cd0cc5d6c4","nfcEqual":true},{"in":"TSPRKRLRMPPUKRMQUUOTLNOLOKQ","isNfc":false,"nfd":"a902ec1e6788cf6e","nfc":"a902ec1e6788cf6e","nfcEqual":false},{"in":"FNNNMMMMMMMMMMMMMMMMMMMMMMMMMHNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"7b3d81774d2e9d50","nfc":"7b3d81774d2e9d50","nfcEqual":false},{"in":"TKTMLMMUULLOQTSTTTNURMLUUVJNRGRRMSOSLPOPVNNVNQMTRKNRMUKVPUSPQNMOQTSQTNGTSVQMVTRTMMUPQUKKNSPRRSOTKQTSV","isNfc":false,"nfd":"e6236367c0a80c8f","nfc":"efd28b1a803b1745","nfcEqual":false},{"in":"GNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"506c52ade03498e2","nfc":"506c52ade03498e2","nfcEqual":false},{"in":"QNNLUQSSMEMKQOFPRQOLETVTNKUSFQUOVRLNLK","isNfc":false,"nfd":"8ebe6c5f0fe7757d","nfc":"34c1d687a1754f92","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"afe7859a0f5d1875","nfc":"afe7859a0f5d1875","nfcEqual":false},{"in":"GVSRRRGSKUPUKMPOSRRSMKURMTRUK","isNfc":false,"nfd":"afe5c24fd12e1d43","nfc":"21d4083e83992346","nfcEqual":false},{"in":"FMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b0626aff5236286d","nfc":"b0626aff5236286d","nfcEqual":true},{"in":"ASTSKSMNLTPRPOSSUSKGNPQVLANRSQQVK","isNfc":false,"nfd":"f5df56c22e5660e0","nfc":"f5df56c22e5660e0","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b0f30b0bfe931db7","nfc":"d98ea1d01c74561b","nfcEqual":false},{"in":"QNSQMLPOTVEVTUQTRLORPPTSPNMMULTRLPPVQ","isNfc":false,"nfd":"23e37f790aa251fd","nfc":"47f1bd6c85a38a8b","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c27ef26315654b20","nfc":"0d08e97e43c3ad3f","nfcEqual":false},{"in":"CFOLVMQSTPMSSBNRQOPTSOLTOSLR","isNfc":false,"nfd":"0891dc8d87f5b4ec","nfc":"c23d215cbf381548","nfcEqual":false},{"in":"BNMMMMMMMMMMMMMMMMMMMMMMMMMMJNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"fcaca60c051139b6","nfc":"9967475ec726f287","nfcEqual":false},{"in":"VLMURPLKVUPLOVTTBTPVSNGQNOSGMPQO","isNfc":false,"nfd":"8b815b6cb0bee0a9","nfc":"8b815b6cb0bee0a9","nfcEqual":false},{"in":"FMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"e45dcaafc09b3757","nfc":"e45dcaafc09b3757","nfcEqual":true},{"in":"POTFULNVUTQRVTSUNULVOKNPMGVPTKL","isNfc":false,"nfd":"1525951dc553618f","nfc":"1525951dc553618f","nfcEqual":false},{"in":"FMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"28d4cc83a1cdc7ee","nfc":"28d4cc83a1cdc7ee","nfcEqual":false},{"in":"PKKRMOVURQVOLRNUMTKKVUPMTQV","isNfc":false,"nfd":"6b4a713729aeaf64","nfc":"6b4a713729aeaf64","nfcEqual":false},{"in":"DNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"32b72bdca53c581c","nfc":"f8734c3eaf7d6fb0","nfcEqual":false},{"in":"KMTQOKOOTTOQQPUSUSURVQUTULPOPPUTLMLLVKQQTVTMLSOEQSNVRTMSQPROVPLKVMTTOFSTVU","isNfc":false,"nfd":"b9bf087bfdd9b845","nfc":"18f10c94a80fcf96","nfcEqual":false},{"in":"GNNNMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"86cca2cda6cdf1fb","nfc":"86cca2cda6cdf1fb","nfcEqual":true},{"in":"QUNPPOMLNSTTPRPLQQMPLOLQURPTVNTNTSOOMT","isNfc":false,"nfd":"a7eab63c08dc3b94","nfc":"a7eab63c08dc3b94","nfcEqual":false},{"in":"GNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1a6f6c6737f7d6bf","nfc":"1a6f6c6737f7d6bf","nfcEqual":false},{"in":"VKRLPSMQQORTPRKFOQPSSSLMLPSOKNPLMVSOSGPNULLPKSPTUVTOTLNUNSMUROVLLMTSKKUN","isNfc":false,"nfd":"ce3d111a9029fef9","nfc":"ce3d111a9029fef9","nfcEqual":false},{"in":"FNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a54c785e376ebe63","nfc":"a54c785e376ebe63","nfcEqual":false},{"in":"OIMSKOPKLONQUOSIONLQNSQLPURKSLSPNVURSSGTVTLKUDRPQLPNVNNNUTQNOPQQOKMLOLVQQVSNOVVPQUSPKRRMKQVSVVMTNSQSUTVLOO","isNfc":false,"nfd":"f6a6e789a7aff87c","nfc":"ecb558646bf9569c","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1b8156ea2c23bc92","nfc":"511c2b0eee04f071","nfcEqual":false},{"in":"QOSORTRNKOOVMTVVRRSMMNKVPKQ","isNfc":false,"nfd":"5a1bd42b4a374ae1","nfc":"5a1bd42b4a374ae1","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"dab0a280cd782d4f","nfc":"dab0a280cd782d4f","nfcEqual":false},{"in":"BJLRVMOBSUKSTKQOKVTSPRNSVATQKRRVPNPSPRVOOQKQTLOFSTHNPRRAQTVSKOTTEPSPVKVKTQTMQLLQPKTTULTTPTPVVMUQP","isNfc":false,"nfd":"e570029a425455d1","nfc":"76781936e1dce74b","nfcEqual":false},{"in":"GNNNMMMMMMMMMMMMMMMMMMMMMMMMMMDNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"57757c74501209a4","nfc":"86aa6f4e04203b5a","nfcEqual":false},{"in":"QJLRVVPOUQNRRMLPPPVPLQJOKUONPRSUVLSTPMNTUQVKLTUUMTSQPEMMUSSQFSLMKQSDDRSPKKUTKVNRMLHMRUTTOSNSPR","isNfc":false,"nfd":"43b7152dac4e2c2a","nfc":"69e987793d88fa0d","nfcEqual":false},{"in":"HNMMMMMMMMMMMMMMMMMMMMMMMMMENNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"5fce95e75101375a","nfc":"f3a92da6e0351eb1","nfcEqual":true},{"in":"KUUVVUVROMNKMLLRNLTKNPMRVLNALSOQNSRVOPUOTLIPNVQPURPVUMPNKLOOHOVKSDVPLONKUQUUKQUUMORNRQVHNOQMLUNRKLVMN","isNfc":false,"nfd":"2f4e7d59e7243740","nfc":"8d37ad26b4be2bf9","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ec42dfa967e8c4e2","nfc":"ec42dfa967e8c4e2","nfcEqual":false},{"in":"NUVQRSQSRVNVTUORPLKTDRRMKT","isNfc":false,"nfd":"d10eb438c5953ffb","nfc":"a1c45114960b7d24","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6ddc95949a3b7bee","nfc":"6ddc95949a3b7bee","nfcEqual":false},{"in":"VTVTUMSMRNOQAVULPPSQKKRNNMKVPPTRUURPLQKPSLTKOPNQQMKRSMNMSMQEOODSFVMUQOKMSURVOBVOONSKMMONRVTVQLOOQ","isNfc":false,"nfd":"d80963385c4301e5","nfc":"fef09c36d2caf8dc","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0fb34ba806c8aa5a","nfc":"0fb34ba806c8aa5a","nfcEqual":false},{"in":"RPVOPMOQOSRSMQRLQKMVSRRQRLSTUTKUSSRKNVSRVKLVMTQNMPRVQRKMPLLRLPLUQMKVTNULTSLUNPSMURVUKTUVRKLTOVQTGL","isNfc":false,"nfd":"c4db566f77f98300","nfc":"c4db566f77f98300","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"16502577c5d28bdd","nfc":"243a8031585ade10","nfcEqual":true},{"in":"MUSSVBRQSMQVQKQVKOMLJUPQTRVUNKVRQRSAKROLNNMSVUUTOSOSOMVKPMLORUKVIQCVKRMTMTOTNQRVTSSKTMVPTQUOOQOPKNK","isNfc":false,"nfd":"c80850d6474f0be1","nfc":"ac7036286b647d05","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMJNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a40a05cc1c015bf7","nfc":"866807fb43539d96","nfcEqual":false},{"in":"PTQQUOPTMKVVUQKKRVVQVSKLRTMKMUNPQMVOVOQSRQNOQMQSVRNVP","isNfc":false,"nfd":"08109b12f8c89b03","nfc":"08109b12f8c89b03","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"f3446ccf22dedb26","nfc":"e2ea25f3bf887b47","nfcEqual":false},{"in":"UKRNOMUKLLPGTRQUVTMALVNNPUPVNKMMUFSMQOJMVQPTRQURURRGUV","isNfc":false,"nfd":"13c716613dac4702","nfc":"e2784102fea6114d","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMJNNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"a54b2c1dd72f8b63","nfc":"0f3be6332eae0d75","nfcEqual":true},{"in":"QDNPRVRMRPLNSQMMSPSTVQUQHKKPNNTPKQRKAMRUOTRSOVPSQLOSQPNKRLPQTQTO","isNfc":false,"nfd":"f76852d2e741973a","nfc":"7bbdc167d24ee607","nfcEqual":false},{"in":"CNNNMMMMMMMMMMMMMMMMMMMMMMMMIMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"8888b769fe0b6668","nfc":"1e633d671035ddf1","nfcEqual":false},{"in":"VOUVSTMPUNUPOOOQURPQPMNMNRRRUSTNTMRLVVTSSMUONUOVVNMSNTUTUTSMQOPTUQTPQOATTSKQTKULOVRMSKSKQTOHL","isNfc":false,"nfd":"7376f1d62ec549b6","nfc":"7376f1d62ec549b6","nfcEqual":false},{"in":"FMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e2480ce0e0be6eaa","nfc":"e2480ce0e0be6eaa","nfcEqual":false},{"in":"RVLQKLRKVRKPRNKVUVRRQPSUMNOVUQTNLPVMPPNVRSRLOKRKUVRKRQKFSMRMLKKQVMOLQQPHQOSUMRLFMPRLROPRSQKONRPPSLNTMLSKPT","isNfc":false,"nfd":"cff4e6f12dd23019","nfc":"cff4e6f12dd23019","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"638182ae1e97e5d8","nfc":"7f6b24352f3195b1","nfcEqual":true},{"in":"KQNVPMTKQNVTRLQNORKRVUNSNUMULUQMOFOTVKMUNAVUTUVMKMPKKOBVPPK","isNfc":false,"nfd":"bf09b4f26719049e","nfc":"bf09b4f26719049e","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"c5bd5bfaba01580f","nfc":"c5bd5bfaba01580f","nfcEqual":true},{"in":"RNTLVUKQPKVMMUMRVQOQRRUUUURMPTLNVSITULTJOVMKTQLKRVUSKNSSPUMS","isNfc":false,"nfd":"74911756e39f12b0","nfc":"faefb57afcc021f3","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"886de1673e94471e","nfc":"886de1673e94471e","nfcEqual":false},{"in":"SOMPNSKUNQKQNQMTTTLPPMSLVKUOTLLUVTOLUSMKKUNMMUUPULVLPUNMOVUM","isNfc":false,"nfd":"a8c2b4b235786bbb","nfc":"a8c2b4b235786bbb","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"6b586e12ab7a981c","nfc":"6b586e12ab7a981c","nfcEqual":true},{"in":"TBOOQFUVLOSTKSSVNMSMMMMUPVNSSUENMOQOSOUVNSVDMRLSRTUPMSTONO","isNfc":false,"nfd":"684df0c3a896eeb3","nfc":"2b1de62fec25bbcf","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"76b8ba63c38507b6","nfc":"6dfbfbdb85ae3c08","nfcEqual":false},{"in":"UQSKQRVMNTTKSOVMTRORLLRPMN","isNfc":false,"nfd":"85700768a2a11d43","nfc":"85700768a2a11d43","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e4d3295204b21f8e","nfc":"7e70528f6eaa1ea8","nfcEqual":false},{"in":"CPTNVOTVPMSUSTJKMONQKRNPUNQUTPKPPTMTGNTQRVVVTSKLQUQMMOOOUNKPKNNMMMTNRMPNKSKPTNUKSQMRQPOKQVUVMRD","isNfc":false,"nfd":"d5ea72608f8dc95f","nfc":"60c4f6894709960c","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"f77a0b2965042899","nfc":"20674ea7c36ead91","nfcEqual":false},{"in":"MCKNTQSOJMVKPSSKFROOSKPNOMSTTISLTFVRPLL","isNfc":false,"nfd":"1cbe1864a45f92cc","nfc":"82190bd571386d2c","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"173f890cf051c47e","nfc":"2559f4bccdfb9a3a","nfcEqual":false},{"in":"QQRORQIKTSUPPMTDIRVPTVSNURBRSMPOLQSMUMKSOKRQOOLNQPRTLQKKORNQRUOQOQMVPNMPQQKOKJMPSTLMUMQSTLSVAO","isNfc":false,"nfd":"9257d4f9150da21b","nfc":"feb35ed3503a98cc","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"8a73b6ca9e572b06","nfc":"cf738a99dc411549","nfcEqual":true},{"in":"VVLBORQVKQCLSSSSVNOTPQOLSQSKMCNLTRGVUNMPQKTSTTQUNTTPLVMOUNLUQVOPKQFRSMMTSLOVQPONKRHOUVTPVPVTMKSU","isNfc":false,"nfd":"fe2d05cf47847f61","nfc":"28a3ac44393a843f","nfcEqual":false},{"in":"JNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"91db2725c3162eb8","nfc":"9d385618580d1bda","nfcEqual":false},{"in":"FSLSTNSANNQRQVOKNVVVOUQOKMVKTMORSSQSKMUTQTMKVUSOMLRJCRSCSKQEBMQUUKNCRTMNRKURMLSMUSRPOTUOUTMQNNLOTSLRM","isNfc":false,"nfd":"68c7bd96250055e6","nfc":"8c0f5d123f97d894","nfcEqual":false},{"in":"GNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"0155f2f473fc8336","nfc":"0155f2f473fc8336","nfcEqual":true},{"in":"VTUIILSUTSRKTNSSMQPOKOVVVMVMVLRVRSMRRJONSKQMLCOVQTRPTNMTVKQPVOROMMOUQROTUP","isNfc":false,"nfd":"4234e7a35ba48559","nfc":"a435e5431382cc40","nfcEqual":false},{"in":"ENNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"5b3782efb77a44e3","nfc":"7ad3f496d64dbbcf","nfcEqual":false},{"in":"TQRPLKFKRLUVSQOGMKSKMKRKLMLNNLSVLLKOS","isNfc":false,"nfd":"50a8d44fb6223b6c","nfc":"50a8d44fb6223b6c","nfcEqual":false},{"in":"FNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c495b7e6da69ff95","nfc":"c495b7e6da69ff95","nfcEqual":false},{"in":"IPKQOQUQPUKTVTNMNRSKTPNTVOMORMVRMKRLNKRSOPLPOQOMTLNVNNTKNNTMUDVKUNVRUJMK","isNfc":false,"nfd":"c2f077a2d787e400","nfc":"51d2c467f4002889","nfcEqual":false},{"in":"FNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"6e82911944cec2ea","nfc":"6e82911944cec2ea","nfcEqual":true},{"in":"PSULUNTULTNTVTNOLQTCLNUOVCRORRLODMVQMOCRPQPMRNTUUORRUKTPOSRBMRLNLLOTTVFVTOUNRAQSROMKQ","isNfc":false,"nfd":"2276d0ef572e9e5b","nfc":"bf5e8bd17001bd49","nfcEqual":false},{"in":"HMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMJNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ce0691aa4d8dc4f9","nfc":"67d9a1dd26926dd0","nfcEqual":false},{"in":"SDTVMNKPVQUOKLRFUQRMQTVRNMNLUELNVPSLT","isNfc":false,"nfd":"0316427e353077e2","nfc":"de96a3662a7e1acd","nfcEqual":false},{"in":"DNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"3ae1f04271e061c3","nfc":"99154acde5f8ba1e","nfcEqual":false},{"in":"GNTPUCOSKONSLVSTSPOVVQTLSRLKTTTVNPPT","isNfc":false,"nfd":"5ddf132bd411fa28","nfc":"5ddf132bd411fa28","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMBMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"400f92c282240bfe","nfc":"400f92c282240bfe","nfcEqual":true},{"in":"LUUURTMKPVLMPTMPTMLUNOVOODSDMNQVONQNP","isNfc":false,"nfd":"7081851aad16e8be","nfc":"4abde784b9ad854c","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"93cf5beebdceefa8","nfc":"93cf5beebdceefa8","nfcEqual":false},{"in":"VMKTSLQLGVNIVSPOSLASSLVPSSSPKORPLQNSSMQQVOOUNQOOAUMTPARLTMLURSSDRNPMVUKTLMTKPMKPULVRV","isNfc":false,"nfd":"ed611fd9e1e77f75","nfc":"11b9c598fb9df6f8","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ec42dfa967e8c4e2","nfc":"ec42dfa967e8c4e2","nfcEqual":false},{"in":"DQPSOOKVLMTOQNLGVHRCQRKURSNNNTQMUMTRGMNQUOLKKSSMKMNVRRROKURSOTDU","isNfc":false,"nfd":"22ac0ac92c5eda66","nfc":"4c40294afbced9f3","nfcEqual":false},{"in":"IMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0ef839f0b1f5f816","nfc":"0c61830c644ef266","nfcEqual":false},{"in":"COMVOPPMKMVQLVQVPQMMRTKMLU","isNfc":false,"nfd":"1816f53317cd96c8","nfc":"1816f53317cd96c8","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMMMMGNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"279080a2f86bbbf1","nfc":"f0461e901aabf775","nfcEqual":false},{"in":"FKKTNLQOUQNTPUQVTVNSVMPOLKQMVLQJJNTSNQPNVOKDSLPTLUKLPSMVNNPNORNVNNRPKQCSNSOSJNTSIVNNONFQLQKPKKMNQNMONSKRQKNN","isNfc":false,"nfd":"4df3121446cee128","nfc":"60b87f4c650e5cad","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"e7f351aa477ba85a","nfc":"e7f351aa477ba85a","nfcEqual":true},{"in":"PPQVLMULKRSQVSQUSQOQPTVTTRLOLLTT","isNfc":false,"nfd":"3696b34981829d24","nfc":"3696b34981829d24","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMAMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"969a0207ad6c5eff","nfc":"969a0207ad6c5eff","nfcEqual":false},{"in":"ICALMLQSMURIQOMUPSRKLOPFMVNSQRKKKKSTQRNOKHVLLRQQPVOOMTVTVRNRMTNSV","isNfc":false,"nfd":"cbab1466b1b97d63","nfc":"6942a8f73969f4e4","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e0deb3447c45ed39","nfc":"e0deb3447c45ed39","nfcEqual":false},{"in":"RSTLMPLSPPRKQKQUKRUNLNVQ","isNfc":false,"nfd":"eaedf2ce7564fe6f","nfc":"eaedf2ce7564fe6f","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b522d212719f824a","nfc":"87aa196559cf25b0","nfcEqual":true},{"in":"QSKRMLVMKTVLROSRRKVLLSMSS","isNfc":false,"nfd":"349dfc35926f1d75","nfc":"349dfc35926f1d75","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMMMIMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0686bbaf4abcc7f0","nfc":"89ba41cecbc98191","nfcEqual":false},{"in":"CUKKUQRSQOUNVFMMBMSKKROUTFAKRKLUPKOROUURUOQOPSKRMLVMOPPSIVTPST","isNfc":false,"nfd":"1610a4d4ccbbc2d5","nfc":"0f461fc902287fda","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ec42dfa967e8c4e2","nfc":"ec42dfa967e8c4e2","nfcEqual":false},{"in":"KTMPTLDMNOUNRRLTSSVLMTMTLSPQQVUMLMRKLQCVTLRAVUKGUMUSNUTPVVVUOMSQOLRR","isNfc":false,"nfd":"adeae1b453183f66","nfc":"f6976cbc442853a8","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMMINMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"3606bafedbfb25c4","nfc":"8b1eab3c10d3ba13","nfcEqual":false},{"in":"MPLBVRMTVLNMRTNHSQSMTUTTSRSTLPTUMMRN","isNfc":false,"nfd":"961fca356aa613f1","nfc":"961fca356aa613f1","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b0e0730f0afeeb6b","nfc":"b0e0730f0afeeb6b","nfcEqual":true},{"in":"NVKRMPUUMQUKPOVSPTVUTURPOTRVRPMTQLUPVLUMTLMSVRVUVKT","isNfc":false,"nfd":"df75dc4dfcafecf1","nfc":"df75dc4dfcafecf1","nfcEqual":false},{"in":"HNMMMMMMMMMMMMMMMMMMMMMMMMMJNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"58572fa31aa6f164","nfc":"e1597b3f12ce41bf","nfcEqual":true},{"in":"QKTKTOMVKMOPLVSPMULVLKLNPTNMSPNPMPCPCTRRVVMPQAMUVQVLPTTVVTRNQSKP","isNfc":false,"nfd":"aad029a5c1c1aa7b","nfc":"aad029a5c1c1aa7b","nfcEqual":false},{"in":"FNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"2b90f924d8a7e524","nfc":"f29f26bdbd3fcc9c","nfcEqual":false},{"in":"RBNNNQQUMPTNVONMLKTSKPMPQRLPUTPS","isNfc":false,"nfd":"afb4bb17cce20986","nfc":"afb4bb17cce20986","nfcEqual":false},{"in":"ENNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMANNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"82a1112701b7d4ad","nfc":"9a5d974e5c3379bc","nfcEqual":false},{"in":"LCTPNUVUOOORVNKTPONKLTMVRMUTFFERFKQVTPNOQQQTKMONMQQLMRUKVRMTTEQLVAQOPKTKPLRKKLSTNLULQSSTSOU","isNfc":false,"nfd":"ca122ab7e17f49da","nfc":"6d401caa5a3a110c","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"ca9ef4adf80925c8","nfc":"ca9ef4adf80925c8","nfcEqual":true},{"in":"IJULQOULVQNROQUTNVQRNPTQNSVMKSRVKNMVPTNKNTNTPKPKRONSNOPM","isNfc":false,"nfd":"f77f4076ee6f390c","nfc":"d428dc90a1e86348","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"e9767215b87b83bb","nfc":"dfcd79c0b0763955","nfcEqual":true},{"in":"ALMVKFPPONQOPNUVVPSMNKRMPLSLSTQMTNVQVLKKKVKORLQMUVVPETLLNUUKQUVPOOKU","isNfc":false,"nfd":"848b89ee7ddf8e58","nfc":"2348c7900a34d27a","nfcEqual":false},{"in":"GNMMMMMMMMMMMMMMMMMMMMMMMMMMMMCMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"6ca9562ec82455c3","nfc":"6ca9562ec82455c3","nfcEqual":true},{"in":"PRSNTMRSLLVQRSPLMQLRUMJRVURRCJRSUTTRKSUQSSKSHLMMMUNVOSNNKSKLTTUQNTLVPMSNUQRRKUUUVNRSVKP","isNfc":false,"nfd":"3eb07c60233fec1a","nfc":"b1172da29dcca8c8","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"10209e4152dae277","nfc":"10209e4152dae277","nfcEqual":true},{"in":"PAKUQOVUORKPRQOSONLRPRPLLLULMFPQPUVVROQOTMQNOTUQMKUSTVHQPSTUN","isNfc":false,"nfd":"e58ac496cbcb4edc","nfc":"a6a178d54123cc4f","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0861562ace09f073","nfc":"684421c10f0a2e87","nfcEqual":false},{"in":"JUIRLMKVNTTMQVRVLSTMBONQM","isNfc":false,"nfd":"16b9ba5611a6adff","nfc":"5fc11d9ebdf21f97","nfcEqual":false},{"in":"ANNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e5ebf0159ce300ec","nfc":"e5ebf0159ce300ec","nfcEqual":false},{"in":"RNTRKKTQTUQOVMTRPMVVTPQKKSVMO","isNfc":false,"nfd":"456c7fa8dac55ccb","nfc":"456c7fa8dac55ccb","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"28b6664c107d212d","nfc":"3e11a96cc23169f3","nfcEqual":false},{"in":"UNGORKVLPMRPPUVSRVLOFLKMVQKLVRUSPSFPVMLVKSMNKOPNVQPKMRPTORRTNSVLUQONPK","isNfc":false,"nfd":"eb586576c2b70e1a","nfc":"eb586576c2b70e1a","nfcEqual":false},{"in":"IMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1ab1e37155f0482e","nfc":"8b22b0de73d5503a","nfcEqual":false},{"in":"IGQKONVKFFQRSLRERSNLKQUSRDPNVMTOQNKNLQV","isNfc":false,"nfd":"586e40e9e5ea4d3b","nfc":"fc9840b6f84298e6","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c1d881c62bd3a529","nfc":"031488e58bbe1eff","nfcEqual":false},{"in":"OQKNRQOSQMLRKQRQTUTSQKVPS","isNfc":false,"nfd":"38119530a46dd8f3","nfc":"38119530a46dd8f3","nfcEqual":false},{"in":"ANMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6271dd3b68e4189f","nfc":"6271dd3b68e4189f","nfcEqual":false},{"in":"AHTVKVOOUUMKMPMNDNNSKTKSMUULMNSFUKQTMOQEVTNSOUUKRSNRTIUREL","isNfc":false,"nfd":"2bdb9416f9a836db","nfc":"32a20048ee1dccdd","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b220901498f8a28f","nfc":"ae2dace2e9c7b64c","nfcEqual":true},{"in":"RQTNUKUSKNPCQMRNSVVKOOLLUOMLQOLPFDPTRTPNSNNTVSPUOPOMTQPUTOOPQVL","isNfc":false,"nfd":"9edeb274e3f1bb3b","nfc":"861f82f18fd42c9f","nfcEqual":false},{"in":"ENNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"dac53168b1ff16eb","nfc":"660b8ecf1bea5794","nfcEqual":false},{"in":"HNOPQKONTKORKSLPLNENONSTLUOSLSVNUMTKSCKUQQSLROTPVMQOOLPQSTUPMVQLLMQMQMRQPLOPSUUVVUUVVTUSKRTPVJSVMVKTMBQOK","isNfc":false,"nfd":"adcee180585d0f72","nfc":"27355600064d7f01","nfcEqual":false},{"in":"FMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"aeb46e01259027ad","nfc":"aeb46e01259027ad","nfcEqual":false},{"in":"LPSQPOVSVLTOKRPLLHPKVQNOPOSQRULNVMNOQLUMOLTPOKKKKKRTVRLCLNSPMOL","isNfc":false,"nfd":"09f14ce8e3ba7740","nfc":"09f14ce8e3ba7740","nfcEqual":false},{"in":"FNMMMMMMMMMMMMMMMMMMMMMMMMMMAMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"aa97ccfa57c98b20","nfc":"aa97ccfa57c98b20","nfcEqual":false},{"in":"TNSUULPSULTLPQPTKVNPCQKNKKQQORVPMVUSMLOVTKKUUUNSVINKRSQTNONH","isNfc":false,"nfd":"2b8eb6b677be7beb","nfc":"5bb15f6aff1f9e91","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c054c53ea3294c02","nfc":"0b1e6d78fd4b4dd4","nfcEqual":false},{"in":"AQQOQMVULQHRPQNKNLTMRVGVNVKMTPOMTVJ","isNfc":false,"nfd":"f5702110ffb492e5","nfc":"b6ac9d74ee9a6595","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMCMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"9a041d8dbad57696","nfc":"9a041d8dbad57696","nfcEqual":true},{"in":"SINOVPUOKOOPPLPOLMMVPKLMLTLU","isNfc":false,"nfd":"8c14827612891fbb","nfc":"07f79ee652150809","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"408dba7b43e7476d","nfc":"408dba7b43e7476d","nfcEqual":true},{"in":"JATOTKUNVMKQVOOUOMOVSTUKPPVSSNTRV","isNfc":false,"nfd":"445f7e338fb874e5","nfc":"b95186fbccd7523a","nfcEqual":false},{"in":"HMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"bb9e527a4cf2db36","nfc":"bb9e527a4cf2db36","nfcEqual":false},{"in":"PNTTSLKMOTCROSQVOPRMSTLKNVSULKSTLLVOUSRORNMVDPMLRUMUKLVMSRPLLPSNULPKLOMTSPLMNDSQKR","isNfc":false,"nfd":"f16fa7e25500b0f5","nfc":"5fdb3eb8ec9357af","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMJNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c733133c7354b47f","nfc":"0120d8f34f013882","nfcEqual":false},{"in":"IKNQSTRVPVLEUONNUUULSMPTKPVLVNVPTULVRLMONSQUKQPSQOTMOKOMLMNVRVUNKNPNROSTSPDUKOMKUKMMTHMKASOSRVUEVV","isNfc":false,"nfd":"45d0f67df735c571","nfc":"4d81b175ac245d66","nfcEqual":false},{"in":"DNNNMMMMMMMMMMMMMMMMMMMMMMMMGNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"9e9bc53f45dc291b","nfc":"e9239358b286dab8","nfcEqual":false},{"in":"VQKUNSQOLPVMVSVUQTUNFKLPRPULMLOOKUUOUCFVPPLTOQSNNKMPSSTOQOMPNLVPPMURKSKRSTOO","isNfc":false,"nfd":"41f02bfd73ddfb3f","nfc":"41f02bfd73ddfb3f","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6380b80021ac12bd","nfc":"6380b80021ac12bd","nfcEqual":false},{"in":"LOUOUFVLUQKRMMTPPNOKKTLLONLQU","isNfc":false,"nfd":"00f6c4713630a2b2","nfc":"00f6c4713630a2b2","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c17d378080107940","nfc":"c17d378080107940","nfcEqual":false},{"in":"LQKQTNOLSUKVNPLMVRNSVVERPPLSUUULVNOOUTLOOQSRKTTTMMLTKMTPPPPMUTLMNPUSIRKNKUTUNNQSRSQASPMKNNMLVDLMNLM","isNfc":false,"nfd":"67785814371802f4","nfc":"c358cd56fb2a3667","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1343597895951130","nfc":"95124b87f80875fc","nfcEqual":false},{"in":"HPROKKTSUKKSLVKPSMNSURSRONEQPURVTTUTQGNMOTMTTSVRNKSOPQOUTSTNROSUNVODNOMUQ","isNfc":false,"nfd":"7abadaa4e608ac3e","nfc":"a960c3029eb56e16","nfcEqual":false},{"in":"GMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"638c78e904e9cba6","nfc":"492290c734ab0d5b","nfcEqual":false},{"in":"DTMOPOQNOVKROMKDKSLVLRMOLNSVPLSNPOVUTK","isNfc":false,"nfd":"e41d6c5b4264761b","nfc":"1d012ccfa45f5543","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"9e30ef72b18b8287","nfc":"9e30ef72b18b8287","nfcEqual":true},{"in":"ELSMTSUVLRUUEMVVTSORMVUSMQOMOVRPQQUTTEONVMQMVSMQVGNQKULETULUNRU","isNfc":false,"nfd":"6314a7931e19d39e","nfc":"fbd063fcf7bf333a","nfcEqual":false},{"in":"FNNMMMMMMMMMMMMMMMMMMMMMMMMMMDNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"38ace8b5be1221ca","nfc":"0bb35cac826f7a5e","nfcEqual":true},{"in":"LQRNUSQTOVTUQPPKVVHSKKKRUVSNSOVTMQEPNOOLTLURMPNNRRNKNVSQOQKSGNONLP","isNfc":false,"nfd":"1ed867e99bfb1965","nfc":"d2ca3a82b79a0eb4","nfcEqual":false},{"in":"ENNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"259219ae5da4a872","nfc":"f1a9451c12883c9c","nfcEqual":true},{"in":"FTTULQPUSVQTSKRNMQQNTPLOKQQTTKNTLSQTNSQKLSNLKMARTNLRTMNOOOBOKOUORQSTOVUMOSOVSRMKVURPSOPSRROSLRP","isNfc":false,"nfd":"059815b8cdd04efd","nfc":"58bca359defc5796","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMJNNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"42e1b8b94ed968af","nfc":"39e47eecf9dd166e","nfcEqual":false},{"in":"NRUNVKTKSPSMKUOLSNTMUNUPNNVMKMMLUQIOTRKROANOTKMPTMLOQTMPNLUPKQQNLMUVMVOMNRUPPTPSUNOQUSTVKRSTKUSRNUMQ","isNfc":false,"nfd":"1d70ea7f5e7b4a5b","nfc":"666b54fe032f086b","nfcEqual":false},{"in":"DNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"3ae1f04271e061c3","nfc":"99154acde5f8ba1e","nfcEqual":false},{"in":"RKLQSOUVTNPVMMMLUMVNKKNOTLKOVURNLLQKMBQLIPOQVVONOTLFBOTRKNNSPULOVPTLUUNKKT","isNfc":false,"nfd":"e9bc98a2397295aa","nfc":"d5cb0416ede3aebc","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"33fd626eebfcba46","nfc":"33fd626eebfcba46","nfcEqual":true},{"in":"EVSSSKMQVLMMTOQLRRKRMURROVVOUMSLNSP","isNfc":false,"nfd":"6ef633f14dbc6969","nfc":"7f26ef67d3393299","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"173468f938afc946","nfc":"2e88c57a149fd411","nfcEqual":false},{"in":"IKKUOQKMQQRNMSLKSNPOUVOOPTLRLQRKVDLUTNLQMQUOQALMPTQVOOROSNPUULULOKSUNPS","isNfc":false,"nfd":"74dda61ddc64ec80","nfc":"bb8db9b593765f92","nfcEqual":false},{"in":"HMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"fb518c7f7c483edc","nfc":"fb518c7f7c483edc","nfcEqual":true},{"in":"UTUPVPPQTKRROMPKUPNSTRUKNKRUTRMSKBEQVDNQNQSSTKUOHNKMKQSTFKMMMVPHVPMSRKVSMPLPKQMSOUPVRQUUPNLNKULOTQVULRVSK","isNfc":false,"nfd":"2bbf4925b0add1c7","nfc":"9b3ec74359a4dcb3","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"af8265b060ba8953","nfc":"43851d49599c02b6","nfcEqual":true},{"in":"TNQTKUQVUTVSKKQNMSRUPFOMOTVNQTURQQSQLEJBNMSVONPLTLNLVUOUNRMNSUONVPSVOULTSUOIPNMPNNVRNMLRMPKUTKVQ","isNfc":false,"nfd":"a3deea96b82e0ddd","nfc":"a294a8af1e5e378e","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"28b6664c107d212d","nfc":"3e11a96cc23169f3","nfcEqual":false},{"in":"GVUVQTMOLLVUPTOVQKLTPKPSURULQNSRVKKLKMUKLSGPQTKPFPJLMPMSQVHNVLQMLQQRTPTHNHRUMMNOURPULTNLLVVNUKNRSOR","isNfc":false,"nfd":"e34f00a448f7292c","nfc":"d8861b8ae2e9c732","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e80f650bb2db4b82","nfc":"e80f650bb2db4b82","nfcEqual":false},{"in":"RKIOQNPQUOSTNPUPASRLLRVPLKTUNUOVRQNPSOJUOOOTMLOQORPJOLLMUTNUNVSMRONPRSOTNTLSEUPLMMPVUUR","isNfc":false,"nfd":"9cda6ba789bbf8f0","nfc":"30914cef28d27327","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e1e801f79ddfbc0c","nfc":"aea0288777dbd63b","nfcEqual":false},{"in":"VUAOTSMPOLVTVTVKSSMSSOSQLNTKSTRDHQUQUMLPTVRKQGUMUOLOOUTNMPSKQRLOQRUKOPNTLLVSNPVTKQRRVM","isNfc":false,"nfd":"48f4436cda4c8f08","nfc":"ed5aa40c8e22dfa1","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b2bcd1c6571cd447","nfc":"b2bcd1c6571cd447","nfcEqual":false},{"in":"SOQIVPKQQSLOLRKMTLUQLPUPPTLCCLSTLKVSMTQTVNLQUSJUOORORPUMLTNOADISUMQRGQRTMPRLQTLSOLOVRMTUMTKM","isNfc":false,"nfd":"3725c4a851e21105","nfc":"8f380b1e64b1a213","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"95b64238b52f358a","nfc":"b5b1a2a0c9ba7c09","nfcEqual":true},{"in":"DDURVQTHRQRVPSTLPUMNLSLTQULRQMPPNKMTL","isNfc":false,"nfd":"a2786538d635a027","nfc":"316d8d67c64c47f3","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMJNNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e7699f68e17668d1","nfc":"9b0b14e3a373e0bb","nfcEqual":false},{"in":"ORUNPMQRPPODNNNVLOPPRQUSTALULLQNLINNNOOOPNONKMQNLSVIPKRPQKOQV","isNfc":false,"nfd":"a3751ff58d5a6724","nfc":"7149ff91049023a6","nfcEqual":false},{"in":"FMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"e45dcaafc09b3757","nfc":"e45dcaafc09b3757","nfcEqual":true},{"in":"KPKMPNRQKROOUKQNQTLQURCBRTPSKRUCMUUMRTOMKORNOTRLVPMUMMATRPSQPOUTENUSQM","isNfc":false,"nfd":"5afc0c2427871955","nfc":"f82004bae4334675","nfcEqual":false},{"in":"FNMMMMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"d12a5d9dd6f73712","nfc":"d12a5d9dd6f73712","nfcEqual":false},{"in":"USNOSMRVNVKQRLMUMJRNUKULQNRPKTQQOIVLRPQONKQMLUVRVQMKVSLKUQTQKKPMUUVUKVUVMKPOTUVPSUSRMOSTP","isNfc":false,"nfd":"0a976d957929e48e","nfc":"839eb399ade59889","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c17d378080107940","nfc":"c17d378080107940","nfcEqual":false},{"in":"UMPSMSTKQQVRVPRRUQTQSLUSLVPSKMUNASMTVVRMUMVMOKPTOULTMSQKCKPV","isNfc":false,"nfd":"b0ba8e812994a9d4","nfc":"b0ba8e812994a9d4","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"eab8d65c287c47ce","nfc":"eab8d65c287c47ce","nfcEqual":true},{"in":"MLBUPVTENUPNOTQPKPANKRRRON","isNfc":false,"nfd":"39892621af99efa5","nfc":"4ae6c812892dd64f","nfcEqual":false},{"in":"ANNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"16c3b4ff81d612c7","nfc":"16c3b4ff81d612c7","nfcEqual":true},{"in":"DCKUQNRRRVNOLTPMLEVUKKQQLLLPPQNFKURRPNOONSMTOOOPLPPVKOMVSPMVONPKUNPSVTKNTRUTQMLONVPPPUPORNMOMRKQOMNHMPL","isNfc":false,"nfd":"a0164577b7e9f314","nfc":"cdd95495ad2b740c","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"61141583d28a9bdd","nfc":"768182b400ed10aa","nfcEqual":false},{"in":"AVQRTNOSRTNOMUSVNPKSOVROPVTTPLOVKURNLTNNMKNQQQRRKRPNRNSMMHQVRBMSLMVTPQPOSIKUTTUVTVLOMNPORMNQKUKMS","isNfc":false,"nfd":"b07bab437cb05fa2","nfc":"9c60ed6002878c6a","nfcEqual":false},{"in":"HNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"c6c11de82aa119c3","nfc":"c6c11de82aa119c3","nfcEqual":true},{"in":"SQNOTVOOUVKMKPOKTPNRNMRTPQLJSVQVVTKTLQPKVLVQPLRRTRLNPNLRN","isNfc":false,"nfd":"309b06db5988b73e","nfc":"76ffd31a167199f4","nfcEqual":false},{"in":"ENNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b64dba1ea87052c9","nfc":"0b8f375d9209a477","nfcEqual":false},{"in":"LRVPPMLRROKSMKTKQPATKSOMVEMOLRQPNKKNSNQRVTQNRPGSKRPMSUOVMSMMMSLVOLVOKELKNOQMUTTQMLTUKVPKTU","isNfc":false,"nfd":"1d39f3b2671817f0","nfc":"8951051752c6fa90","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMANMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0622cb270411ec9e","nfc":"0622cb270411ec9e","nfcEqual":false},{"in":"BVULKOPRUOOLKNQURMPMLQVONCKPSORQMLQUUSNKTKQUNOVTSLKPPHKLCQ","isNfc":false,"nfd":"094e60de55e92e69","nfc":"094e60de55e92e69","nfcEqual":false},{"in":"IMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"18fc7a509ae11575","nfc":"16a60865cc0c918e","nfcEqual":false},{"in":"OTRMMOUUKPIQSTMTSORTVRPSLMVM","isNfc":false,"nfd":"4084495d23b2c22c","nfc":"e119e352ad432643","nfcEqual":false},{"in":"CNNNMMMMMMMMMMMMMMMMMMMMMMMMMMBMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"d79f49a9194eef44","nfc":"d79f49a9194eef44","nfcEqual":true},{"in":"UROKTTVNLTRMOTSTDVLSRRQUOMQODOKLNTKOPPPSRMSUUSQNTVNRPKRTMLVSKPNMAVRNNRVKRPNTKOOPUMTUOMNVKVLQNKOQQSM","isNfc":false,"nfd":"5f6157eab80415e7","nfc":"3e8d868c8ca6d837","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a30426dfd43ba6ff","nfc":"ba96d8b8d2fbdd03","nfcEqual":false},{"in":"GMPKRUSSDRUSMLVPOTOSVLSFKRMRNOKORLTSTTCVUOTVLKPMKMMKVHSNK","isNfc":false,"nfd":"e9dfb092ba595894","nfc":"3e7b8d61fe7602f0","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMGNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"6cc4df7f2a5bac18","nfc":"6cc4df7f2a5bac18","nfcEqual":true},{"in":"QDVTPQVUKONQTSSKKPNPCSMUKRJMLRGKOKPTURU","isNfc":false,"nfd":"7de66486c5783f9a","nfc":"5ea0bed988b31041","nfcEqual":false},{"in":"FMMMMMMMMMMMMMMMMMMMMMMMMMMMHNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"262d3f2fd4fd5f36","nfc":"262d3f2fd4fd5f36","nfcEqual":true},{"in":"TBOUOTQKROVMNLSNMSVPLOSVORUQKRMLNPRMNQLQLRRRMUTORURSRQSUMLTHUASMQJV","isNfc":false,"nfd":"d3a59360dee68654","nfc":"17c6e05324041a1a","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMIMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"a895caf5fd2ead26","nfc":"55408b6a1a3b2bd9","nfcEqual":true},{"in":"LKPTVSKTQSQNSMNNMPULOVLPKULVRLMBTNTORQLPQUMKPISPMQNKNNLNMSTOSKRORR","isNfc":false,"nfd":"311ca31654d4e96a","nfc":"38c455c99b003a0e","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"25088140e02f1f8a","nfc":"25088140e02f1f8a","nfcEqual":true},{"in":"VPVNUNVSMNLNQVMUUUSMPLLPMUKKOOSDUUTQPT","isNfc":false,"nfd":"2e48ffa10c94f6ab","nfc":"f27603f05dab1428","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"58aa5ba7a7b8567c","nfc":"0727d6230c5c67db","nfcEqual":false},{"in":"KUKKKKQMNQLOSTUMVTNTAPUVLLJN","isNfc":false,"nfd":"9894555b6534258f","nfc":"d98d137d3d0c9852","nfcEqual":false},{"in":"HNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"c6c11de82aa119c3","nfc":"c6c11de82aa119c3","nfcEqual":true},{"in":"PJUVSLNQLVMKBTOKNTUQNPRMMKTLNP","isNfc":false,"nfd":"fc83778b5ec63d0e","nfc":"2ef9d2aac0ea2eb2","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"ae0016cb965166aa","nfc":"ae0016cb965166aa","nfcEqual":true},{"in":"GQVNKTLNILTOSKTOMEKUVRSOKT","isNfc":false,"nfd":"decf4411f2e1d4e9","nfc":"b76b6ae8dc26ea2c","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"16c19f8e97c175e5","nfc":"ae4c436c651ef63a","nfcEqual":true},{"in":"KSPTLSKRRKSRSRSRUPPMKKESLTRPMOMV","isNfc":false,"nfd":"a9be9cf975a439f6","nfc":"1150693f6d9c21d0","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"44cd5c61966f206a","nfc":"7dff00b2d4f260aa","nfcEqual":false},{"in":"OSHFTTKKKVNSRUUSSOVQNRATNVRPHVKQOTRQLFMPMRMRSQKUTTMVKLUVVMURVSMRORLMPKSPKVSOKVLUKORROMPSSVLSOSKN","isNfc":false,"nfd":"df3d2adf059f6bcd","nfc":"df3d2adf059f6bcd","nfcEqual":false},{"in":"IMMMMMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"2b69b5ab26288c26","nfc":"0d3e10be969555d5","nfcEqual":true},{"in":"BKNPUNNSVLMLSQOKLOMQKOVOMSKSLRNSOUSVMOORPRTSROQOQEONDTNRLGNTSPUNNENRMKKSVTVPMNRUMNNPUK","isNfc":false,"nfd":"6b759f7a7f253689","nfc":"e352fbb68dc99ffa","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMCMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a1c233babc8badab","nfc":"5aa557928999395e","nfcEqual":false},{"in":"EPMSNLQRLMVNPONLLPQLOTLOLKRSOHGSVKUVPRNQLVSVALRSUNSSTPSRKSVVQUMNNPNPULQQCSNLPOLUOVTTLRORMOVKVASPOOMMVQKO","isNfc":false,"nfd":"ab5bd22d64e3b0d5","nfc":"444cc7d3ce0ebd52","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4dec7457edaa955a","nfc":"4dec7457edaa955a","nfcEqual":false},{"in":"HUVVURSTRSLBSQPLMUVNRTOOKPMROSSKIUSTUMKVSPVTTLKCKNNVTPUOVMO","isNfc":false,"nfd":"18491e98816cdc6c","nfc":"32f556f5ade77b51","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a24fa7b896615b14","nfc":"a7c52eaeaf0c2709","nfcEqual":false},{"in":"RQRMMTNMOKNKPMVSVSMQSKRKPMMPLVOLNPLMULUKKTGNVAOUOSNVTQQPSOTTNMVQHSVQCHKSRQPLRKLRVPLRSOPOUTTVVKUTKRTN","isNfc":false,"nfd":"4479c79501c0dacb","nfc":"a05f15daca8dd68e","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"c7a29342093593c4","nfc":"5c496914a84c413c","nfcEqual":true},{"in":"PDURPUVUMVRRPSRPKMPNUTQULNTTLPTLDSRQMRLTSSQNQTSQNQFTRTVNLTPUL","isNfc":false,"nfd":"a0dfd7281456da52","nfc":"0c1bab4a9854154b","nfcEqual":false},{"in":"ANMMMMMMMMMMMMMMMMMMMMMMMMMMANMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"9e0a87c9a93ceade","nfc":"9e0a87c9a93ceade","nfcEqual":true},{"in":"JVQQAOTKUUVKSKMRLVOOQONLNTPPTPKTKULPKMTLTOLRMKRQMUOSOVPVLPPRUSNL","isNfc":false,"nfd":"41f57586aeeeb8ba","nfc":"dcbb49e0f2bef088","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"8247e7824fb4211c","nfc":"8247e7824fb4211c","nfcEqual":false},{"in":"CTKLOLMUTUPLUMQUSNMQNOKUPKSTEULOTVSPUPURMKSRKSKPKTLVMPL","isNfc":false,"nfd":"af0602236efd8e19","nfc":"de7ed3b903f90a6a","nfcEqual":false},{"in":"HMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"363d183871e4ada5","nfc":"363d183871e4ada5","nfcEqual":false},{"in":"ORULUPPDTKJPROMPPUTPQNKVOPMOQL","isNfc":false,"nfd":"baf17b9bfa5bfa39","nfc":"087f1529c2bc46ad","nfcEqual":false},{"in":"BNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"a7d4bcec0ac07b9b","nfc":"a7d4bcec0ac07b9b","nfcEqual":true},{"in":"MNNNOPRSNLSVPLNOLLMTTMLQKDQTPHSVTUSNQUPOUQUVRVNSRTRSOVRNUVVSRAUSULOKNRVMUOTPONTPSUPBPSKRLMMNMMMSTM","isNfc":false,"nfd":"4954e34aebf504b5","nfc":"8cf521836f987d1e","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b8f4759b80ab8c53","nfc":"950d60abc04c305b","nfcEqual":false},{"in":"IUNVSKMUVKSQROMHKPSKVUSSMSFMNVLRSN","isNfc":false,"nfd":"dd02ba30f2843e76","nfc":"9c19b7162251d614","nfcEqual":false},{"in":"GNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"748ae57fb62c1431","nfc":"748ae57fb62c1431","nfcEqual":true},{"in":"GRSUOLSVNORPVNUVQMQUMVGRRQTOMUSKSSLMORRAKNTMOSRQRMKVIRVGTPGMTQVL","isNfc":false,"nfd":"20e312282185a984","nfc":"f236d13afdb35f2a","nfcEqual":false},{"in":"CNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"73e2f729d1500860","nfc":"73e2f729d1500860","nfcEqual":false},{"in":"QKPTRUNNQUQNSNKVSLLOHPPVTRQRPMRTMVTVOULRNLTTOURSQPVTOLQKURVLNOLLRNULUULU","isNfc":false,"nfd":"64063e8bbb27e199","nfc":"64063e8bbb27e199","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4e7a411c058d8fe3","nfc":"4e7a411c058d8fe3","nfcEqual":false},{"in":"FBKSSPOKNUUPORLOVPNUPGNPMMMLUONPUHPQTUPAUNTVLMVVUTVORNMQLUOQPOGDTVMKTLPLVRONUKLRQOQVLTQQSPN","isNfc":false,"nfd":"c5594b33556a8ec3","nfc":"78fd2797c0e6b4ce","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMMMFNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"df5924af2c3932cc","nfc":"df5924af2c3932cc","nfcEqual":false},{"in":"KAVUPRKUUMNVOPSPUPMLRQRTTNNQULLLUNUVSSRUTUMPTLKVOPTMMVQVVKPTTMQT","isNfc":false,"nfd":"689bbb62a134a74d","nfc":"689bbb62a134a74d","nfcEqual":false},{"in":"DNNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"895df7990dc7da3e","nfc":"b893c631a89f5244","nfcEqual":true},{"in":"VFSMMMVRNNRKMKUMKQUMPTSQLPULNRUCTQQRNKLRPRQVRLSNQTVNKLVTPSHPKBOLNUKPQTTTMUSSNLLMKRSMVMPRTKPUOMOQ","isNfc":false,"nfd":"7f91e56c37260eee","nfc":"f2a151868efb948b","nfcEqual":false},{"in":"ANMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"f8fcc47dab52d39a","nfc":"f8fcc47dab52d39a","nfcEqual":false},{"in":"TKQPKNRQTPLRKRLNSUQPLLMPQKULKSRSLRCSUOQUQVOROTLPRRKSOPSOUTUSNSVRTLPPPTQBRVSRSMVTVOSRKUQRRMSUNUNRLMU","isNfc":false,"nfd":"bd0b67c48d1dd272","nfc":"bd0b67c48d1dd272","nfcEqual":false},{"in":"CNNNMMMMMMMMMMMMMMMMMMMMMMMMMMBNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"6f244e82fa967246","nfc":"6f244e82fa967246","nfcEqual":true},{"in":"SKNTSPMRVQPHNMQUTNOUKKOOLUQCUTKKTKUVVONQTLTMOUKTUHUMNQRRQSMRSLRTMNKTKVHSQQOPSKSUIMHQPL","isNfc":false,"nfd":"4ccd2692beaeb396","nfc":"dd1e25396186ec23","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ec42dfa967e8c4e2","nfc":"ec42dfa967e8c4e2","nfcEqual":false},{"in":"IMOUMLPRLVUVTLKTLOOQPLMSPSTVMMRSQPVPSLOPOKRTMSOKNRQRUVURVU","isNfc":false,"nfd":"5141a3e16dd0b8a0","nfc":"05266cb98596dfee","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"32633eeb93fdf02d","nfc":"8b061515399b9f50","nfcEqual":false},{"in":"UINNILOMQQOQTTPTTMQKTURPTINQUNMPJVSULMUTPERKLLSUMLULUUVQVN","isNfc":false,"nfd":"e626d21bde972337","nfc":"00e47b907251c73d","nfcEqual":false},{"in":"DNNMMMMMMMMMMMMMMMMMMMMMMMMMHNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"36bdc84dddcc7585","nfc":"f2639559f33789b5","nfcEqual":false},{"in":"QPSMSPGKVVRMOOPOQRTVUONNCLVQQVVOSVSUTLLTUOVLTLMRSNOJVNLVVTMNVMPMLNPN","isNfc":false,"nfd":"a75cb58aacf0e63c","nfc":"cf8f0b2ed680a3e1","nfcEqual":false},{"in":"DNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"98c3a1fa8224686b","nfc":"75bdc7f9a47da383","nfcEqual":true},{"in":"PQNSQPVKNLOSMLNVTMNNPQPQKPUMNKVKPODDLGOMKNFPNPLQKMQNOOPOLCRPMUSRRPTRTRLOUKPLMSQTNLQNMQPMVQSV","isNfc":false,"nfd":"508aa0f8aac54845","nfc":"2dd437334294737b","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c0e32c033f162c82","nfc":"c0e32c033f162c82","nfcEqual":false},{"in":"MPQCOOUVKPGPTUUGRMTNUVRVKRUMPVNKLTNJMMTSOSQTVRUVTPLUOGRUNUKUKLNMUESKRSSVMKUMLRLTLPPCTMMKVTOSMLRK","isNfc":false,"nfd":"c396404f2740e5dd","nfc":"a944a65ec05cc2f1","nfcEqual":false},{"in":"ANNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"dfa6efe56aa49848","nfc":"dfa6efe56aa49848","nfcEqual":true},{"in":"VMSPTMPSQVJUNTRTRQLULPNUVULLUV","isNfc":false,"nfd":"65b072b9ff95f3e8","nfc":"57c01ccccaa5fe6b","nfcEqual":false},{"in":"GNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"8fbb50919bcc911d","nfc":"cb133155f7df12bb","nfcEqual":false},{"in":"STPLMSOOQQMVNKVNTLTPOMKLMSHNHPMRSTPRPUSQPVVLRNEPKKLDKMKTQMPMPNVTRMRUPSKVSNVONTNMKSSQUUHVVQSRE","isNfc":false,"nfd":"d363826b8b5e98a7","nfc":"1e10e56253ec3be8","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMMHNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"d3ccf9cff46f802d","nfc":"d3ccf9cff46f802d","nfcEqual":false},{"in":"RKPQULVNTKKSQVTVOQTUJPPLKNSSUNTONLUQSMPVOTRQRNQKOLSLSPSVRQSTTRTVUOSULMKRQKLSUNVMRVEVUMP","isNfc":false,"nfd":"ab25f183d5e46029","nfc":"f2cc39a970b6ca92","nfcEqual":false},{"in":"FNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ea86eb918eb26d33","nfc":"ea86eb918eb26d33","nfcEqual":false},{"in":"GQSMNSPOTNKGVNLMOLKOLRONLSSKSTSKLOUCLTLNPVUOVLPUQLTPQRPNAULUSQOTRRSKKVPLVUSOUOUIQRMSNTOA","isNfc":false,"nfd":"f7d61f0da81b3d51","nfc":"9f43f40e1188f894","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"93cf5beebdceefa8","nfc":"93cf5beebdceefa8","nfcEqual":false},{"in":"SUSQRVSPLKLTOTKHRRNNKOLLPLE","isNfc":false,"nfd":"8020b51116d40b04","nfc":"a0e84b800fd29082","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"5e3731aa3b998be8","nfc":"5e3731aa3b998be8","nfcEqual":false},{"in":"GKONLSOKVISOSMQLLVQSQVQNTKR","isNfc":false,"nfd":"7840ce608cdaf1f7","nfc":"a7798c6f455e4257","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"45fd3dbc3a976e9c","nfc":"454c2d86dbf52f3d","nfcEqual":false},{"in":"OKLRHUKSVQQLPPTNOOKLRQTOEHUTQKKSRQRVUP","isNfc":false,"nfd":"af4c5bea93983db6","nfc":"4930c65e6ae231ac","nfcEqual":false},{"in":"GNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"506c52ade03498e2","nfc":"506c52ade03498e2","nfcEqual":false},{"in":"DHMTSPLQURQKURVUOMKTSSVNSPSUKNRVRKNQVQRURLTPLOKVMQSNVLRRSTUNTNVLUPLSVSN","isNfc":false,"nfd":"cee2b5054c4568d7","nfc":"1274ce135765a7fb","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b394a2a62335ba72","nfc":"b394a2a62335ba72","nfcEqual":false},{"in":"NOPMRKRPPKLOBLBQANURNNLRVRPMVPJNT","isNfc":false,"nfd":"eb3aa5c7458178ce","nfc":"7d8074ba66075ecf","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMMMGNNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"83a962da2feafc30","nfc":"84b953ff9504d9cc","nfcEqual":true},{"in":"PAUKRLNSKPQVULKKPSSTSVNTNUNPSNQKTVQLVQMMLRSKQOTVKTHLPSUNKOLOLORLUVPRLNONVTOMVTTMNVNNNSLUSTTLMNSTTV","isNfc":false,"nfd":"289fce9a6c9a9ae3","nfc":"289fce9a6c9a9ae3","nfcEqual":false},{"in":"GNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"11e156d39bf96413","nfc":"11e156d39bf96413","nfcEqual":false},{"in":"FTMPTGMUQLVUSKLVMNRSUSUMKUPOPVKLTVKRSOTVSUKKNQVTVUSOOUVUOLKPBIEKUMJSPLRKMOUPVSLVQULPSQKTRSN","isNfc":false,"nfd":"ea979650589f5371","nfc":"c30cfdb8c1337dc6","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMHNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c614b149b8a3a16d","nfc":"c614b149b8a3a16d","nfcEqual":false},{"in":"PSPTLKPPUTTPOVLQVKKSMNDTUUVOTLERKOKODNPQSOPMLQOUSMLQNQLGNKVPIOTQQSSTMQPNLRLQTLNTNRUQV","isNfc":false,"nfd":"62a85f75defa32e1","nfc":"7a951c709aba1baa","nfcEqual":false},{"in":"FNNNMMMMMMMMMMMMMMMMMMMMMMMHMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ef039e6e6fcc3d6b","nfc":"ef039e6e6fcc3d6b","nfcEqual":false},{"in":"JDMTNJGPVOVUTTRPSQNKTTTNMUUKUVLSRPORNLNKORKOSLNPVPNOULSUOUSVQNTQLOKLNAVMTSRUVNTMDUMQOFPVQTLTRVLO","isNfc":false,"nfd":"c90e1c9a9694bd67","nfc":"e3859460084a2a50","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"9da1f265cc558b21","nfc":"48386e7704de24ea","nfcEqual":false},{"in":"TUVTPSOMUKOOTSAPROKLRVUKQNUTLKRSTNRNUOKHPSOVOOTLPTKNSPNSTNMVSLLNQQLUPSPNMPURUSKLRRMMP","isNfc":false,"nfd":"4105c22af727bda8","nfc":"169832bdce91c668","nfcEqual":false},{"in":"GNNMMMMMMMMMMMMMMMMMMMMMMMMGNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"0903fdab3b81dec9","nfc":"0903fdab3b81dec9","nfcEqual":true},{"in":"PQPNMRNPUSRLRTSQPPUNMMULQTTMLRKMKOIUOTURBKNUNMRONOVMMQTQLNQQNPVPLORSENUMSOKVVVPLKTQVIPOIVPS","isNfc":false,"nfd":"8bb9951fcf8bf459","nfc":"cc0ce55e37101fbc","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"354910de1a088650","nfc":"c050f5db91607cd9","nfcEqual":true},{"in":"LVTQQPHUUOPMPUMPLNUPLMNTQSLSNUVTTDUNVKOVNKULSKNULNQEMKRKRLNOSO","isNfc":false,"nfd":"1776c35771f64046","nfc":"c53da10480a7a0a3","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"886de1673e94471e","nfc":"886de1673e94471e","nfcEqual":false},{"in":"SPVKNSNOOOPTPKUPMTMKTKQOSTSSMH","isNfc":false,"nfd":"2ab415bc9831f381","nfc":"2ab415bc9831f381","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6b21e24d70072d59","nfc":"6b21e24d70072d59","nfcEqual":false},{"in":"MBNTVMSRVPNUQLNURNOOPUSKSQMLTTQQPUPAQOORNQSSSSMUNTNRELLMPUPTNTVOVKOTRPKQUMPUPOJKKTBPPMVOULQRMPVPLU","isNfc":false,"nfd":"460acebde3c1603e","nfc":"9aa1f6919189c280","nfcEqual":false},{"in":"FNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4bcabe9ebcdbc5e5","nfc":"4bcabe9ebcdbc5e5","nfcEqual":false},{"in":"BTKPVKTLRKQKAKPSQLIRTMQMOKNPJMMUOLRKTPVNSSNRPUKNOULTQQMLOTLBQNTKHKPTMQUROUOKKSOPUQRNNVKRRNUKKOV","isNfc":false,"nfd":"7f1eab76ea6b8972","nfc":"57ee98c7f217b50c","nfcEqual":false},{"in":"FMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6cfe68c27855bd79","nfc":"6cfe68c27855bd79","nfcEqual":false},{"in":"KCSVMNVRRRMQMPKTNMORKLPOLRKMOQUNKPKORLKBVKQIKSSQTMQTSTMUVRQTPV","isNfc":false,"nfd":"8a031d80cae2b268","nfc":"e167b9fc60c5338f","nfcEqual":false},{"in":"GMMMMMMMMMMMMMMMMMMMMMMMMMMMMMINNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"f276ce2ff11d8a3e","nfc":"9d5951b5a279be5a","nfcEqual":true},{"in":"MROVRQRTSSRRKTMQUSOSPUPRURSSQOMLPENPOTKKRTTRRPLSKLOVOJQLNVMUQ","isNfc":false,"nfd":"4c5c56afaf31c7a8","nfc":"beb7595ddc3072d4","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"87a5dc2bea25aa02","nfc":"d1d8932c08e649cf","nfcEqual":false},{"in":"UUTKVMPLQUMTVKSMRNTPNNTQSUQMHTRUUNQOSOKLURLQMLSSRSLRLRVKVUNVUOURMTRTQRMUTMSUQVMKMKNUTUBKPTVQNQRVMOQPPPKFTNQ","isNfc":false,"nfd":"a99d14152f008ad0","nfc":"44ef516ed283fe09","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMINMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"86ed731053c19625","nfc":"322e61f4784cdf94","nfcEqual":false},{"in":"GCOMOQCSTOMCRNNLVNPTTPIPRQRQRNUULNNNLTQKMNSQTKNLKOVMOUROQT","isNfc":false,"nfd":"c14a5482dcd30b62","nfc":"5d7a0fde97df33ca","nfcEqual":false},{"in":"GNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a7fddefe8d71ae64","nfc":"a7fddefe8d71ae64","nfcEqual":false},{"in":"PLPSTMNTQOQERROVQNKTTKNKKKQQ","isNfc":false,"nfd":"9c6d901eef2abb00","nfc":"939fc325c2128b22","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1c5be75d5a167b17","nfc":"d915fb3f15cc5c0c","nfcEqual":false},{"in":"OMOROOTKQPNMUKPLULVMLMSNONUUTSRTQOUCLSQMIQNOUURTKDVIPTSMMILRVSGOKRQITLTKULOQKKTRVLUKLPJTSQLOOSKMROLONKOTUQLT","isNfc":false,"nfd":"632a2fb66f31960f","nfc":"4c1dec1a0863e08b","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b522d212719f824a","nfc":"87aa196559cf25b0","nfcEqual":true},{"in":"DRSULSOQTQUPKTVEUMOLLPVLPKSTNCROPOSNSLMLQMPQRRMLMPVMSRPKTTTUKKRLSSTLPU","isNfc":false,"nfd":"e51c28835d6cfc08","nfc":"d91c3a0e291aeea7","nfcEqual":false},{"in":"DNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"22fd1249fe4da103","nfc":"854aabe4b0e1228d","nfcEqual":false},{"in":"NHKOSKMRTNQPTSMQRNKKUOMNPTUSUR","isNfc":false,"nfd":"c7745d6f317bf881","nfc":"c7745d6f317bf881","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"4cdd65a95170f8e3","nfc":"fc0b317486cde107","nfcEqual":true},{"in":"PQQKPVTLQMTTTUSSHOVSMTTKLKRKNQUVNRPNVTR","isNfc":false,"nfd":"90c549343c639531","nfc":"90c549343c639531","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"2039167d64d2c729","nfc":"2039167d64d2c729","nfcEqual":false},{"in":"NULLMTQPKOSVVTMKKULVRUNNUFKOULQKNRRKOSVQNQNOPQQNSOKOMTS","isNfc":false,"nfd":"284a2b0c53c73fc1","nfc":"284a2b0c53c73fc1","nfcEqual":false},{"in":"HMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"a4ea9bcf4e8bd3ea","nfc":"a4ea9bcf4e8bd3ea","nfcEqual":true},{"in":"QBVSMOQRVLLVMMSLMPTNUTNTLOKSUPUSNV","isNfc":false,"nfd":"308abd304aec903a","nfc":"308abd304aec903a","nfcEqual":false},{"in":"FNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"16bfb79bb93b023e","nfc":"c18e3204f1b38d0f","nfcEqual":false},{"in":"UQNVKLVSMOOMVPKMVCJTPLUVQKR","isNfc":false,"nfd":"5bb2fa6026f342f5","nfc":"ab70fa2d7c473f1a","nfcEqual":false},{"in":"HMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"8721591d39df6528","nfc":"8721591d39df6528","nfcEqual":true},{"in":"DPQLOUONTPUPRNPLSFKNKPRSHOKBOMPQKQLUGVRSVPRPKLKMQ","isNfc":false,"nfd":"83fa6fb599a3d333","nfc":"d85d4f2e4d9dd07a","nfcEqual":false},{"in":"IMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6823e3d1466dcad6","nfc":"14fc5b9265e8e4f0","nfcEqual":false},{"in":"MLQGNTOSRKTVPNSELMMPNTSQSQSMSTNRKNLQSQRTTQULUVPLKLTKNPTTQSTPV","isNfc":false,"nfd":"539bc0263eb35957","nfc":"e947cc6cb320583e","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"45ab7de69acbe42a","nfc":"b7c4772e335209b3","nfcEqual":false},{"in":"SKKNTSVRNULKQTLKOVNKSROOCQKUJVQNVNSSMVNMSMSUOULTOKUUPKSKMTUSJALQTUVMONPRQPKNQOPSVMMNRMKRCPR","isNfc":false,"nfd":"1ecf1d7205c25c1a","nfc":"8a0a6571f64b1614","nfcEqual":false},{"in":"DNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFNNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"df81458b8e03e1e9","nfc":"64de7274b364a583","nfcEqual":false},{"in":"KMRUNLTMPQSUOKUOPLMVQKQIPLMOPSVPTORQSTSMLKLMPTVNQHRNMRQMOQPTMTOUVRKVMRSTNOQKNKUVUMQRRRSVQKKQL","isNfc":false,"nfd":"3e05255a362053ef","nfc":"65b3fcec2c022b49","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMMMAMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b828ca977190811f","nfc":"b828ca977190811f","nfcEqual":true},{"in":"BALCMFURLQUUMVHLOORUUKKQPTMOTTSPMNOFRLPRMRSLOVOQKVNUUNTVRMRUV","isNfc":false,"nfd":"e605b53cbd64db9b","nfc":"da8e4518756ff52c","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b39984a96d4a1a5c","nfc":"b39984a96d4a1a5c","nfcEqual":true},{"in":"QVVSORUUSVQVMNUVTNMOMNKVPONMTSBUNKSVQQSPKSULUTQUUPROCKQLOLQRMNKLOTURNUSOOOONLENQRTLNKNLKQMS","isNfc":false,"nfd":"088a3fc57a42a693","nfc":"64e3dba0040697f2","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"662350073e53b7ee","nfc":"662350073e53b7ee","nfcEqual":false},{"in":"CMQMLUHUSPKUVQPKUKUULUNSPVQOVORNQO","isNfc":false,"nfd":"eacda6e02fc15e3e","nfc":"eacda6e02fc15e3e","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMENMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e1f6548ecab86ce2","nfc":"9d68af6c4e05f9c2","nfcEqual":false},{"in":"LORKMUKSSLKSOVQSPLRKKPJRURMURO","isNfc":false,"nfd":"66def21547ed4e9e","nfc":"c46769786119dd88","nfcEqual":false},{"in":"FNNNMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"a64edc0f8dc45f28","nfc":"a64edc0f8dc45f28","nfcEqual":true},{"in":"PNLPRLMOSOOKKLUSRRVRUUKOOKKROOUIKVSUSVSPLOTNNPQQQQRUOLQLKDPNMQLUSLLRTMKPSTTRMNMSLOPNTOVKURMTOT","isNfc":false,"nfd":"fb2950ccfdda7c52","nfc":"4709add65ec8a696","nfcEqual":false},{"in":"DNNNMMMMMMMMMMMMMMMMMMMMMMMFMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"6432110723563ac0","nfc":"908736f61b6736f0","nfcEqual":true},{"in":"KQUOBKMVTOLNRPMPQVMNOLNRQVQVVOVTUHPRUQPSQSPKMLNPTUVRRSTNNSRKMOPOPKSV","isNfc":false,"nfd":"dcfb77b985c03abd","nfc":"26bb47ef0b1cf041","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a55998b439e236ca","nfc":"a55998b439e236ca","nfcEqual":false},{"in":"PQFLMNNUKRVJLKSTKQRPGKLPOOLUKVKVKNV","isNfc":false,"nfd":"000e118dc0bfdeec","nfc":"b994c855f2a4e6a2","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"260ff38b277ce454","nfc":"260ff38b277ce454","nfcEqual":true},{"in":"ITTQMKLSMLPRLPVOKQVIQURVUKUPSRMVIUPTMKUPFOQMVMOPEKNRVVPTOQUMRNURPVVKTVVQQAUVRUNVQTQOSNVSNSQQMPKNPO","isNfc":false,"nfd":"71fed82e3dcbcc01","nfc":"1a2e0568cfc6ada7","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"db300176f7c16833","nfc":"89690eeca7b5480a","nfcEqual":true},{"in":"PRPQNPSQMKPQKMULULLLSPMRSTKQMRERKMULNUPMTPTNNARQLTOVKRRKKPNRFLUPMRPROPSNKLLSQKMOPULSVM","isNfc":false,"nfd":"3803227eadbb07f5","nfc":"061538da1d3612d7","nfcEqual":false},{"in":"BNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0bd4c7d9cbc89931","nfc":"0bd4c7d9cbc89931","nfcEqual":false},{"in":"LNOLQMKOMUQPVPPUPSPLRTKVPTPMOVUTQRHKPSVURKSRQLMUVBMRQPLLVJRRTLKPRQSNQTOIMQUQMUSLMULQMVNKULONN","isNfc":false,"nfd":"bf306be8f96b34bd","nfc":"63033caa22431bf5","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMMHNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"e6abc39ba0d4bf1c","nfc":"f1b40144c113892e","nfcEqual":true},{"in":"KULMQPMQMPKKPTORTTKQONORV","isNfc":false,"nfd":"ecc9f7642a949d91","nfc":"ecc9f7642a949d91","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"11bc08bef1841141","nfc":"17f769cb09fbdb5b","nfcEqual":false},{"in":"ASKQUSQPVKSRNJTIRRLTSOMRLQKSVNMSRKUTNOQRVOPNLNNOMVPUPQKQTLLMKROOOML","isNfc":false,"nfd":"fc7fe01bfdf39030","nfc":"0ae78dca1a419099","nfcEqual":false},{"in":"HNMMMMMMMMMMMMMMMMMMMMMMMMMMMENMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"1e4c95af590a3d87","nfc":"925cf0879c58feb4","nfcEqual":true},{"in":"PKRMPNPKVLSURNRSNGPPOMNNOPRPLRP","isNfc":false,"nfd":"d67a1409ff27dfa6","nfc":"d67a1409ff27dfa6","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMMMHMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"bc67bc13aff96213","nfc":"bc67bc13aff96213","nfcEqual":false},{"in":"RMSTOKUUKLLDPQOSRLKLNKTLKOARNSMQVRNTKRPEKMVSKQMMNQENSSRFPFRVMRSSVSVKP","isNfc":false,"nfd":"eb9909be8a17b210","nfc":"0fe42395c60e5826","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"008f0e97574e39e3","nfc":"316fd3a678b1b534","nfcEqual":false},{"in":"ONOLTNTLQPRMKAKULMOKORPPSPSPMLTSTMNKESSTTNMVUNOQLMTJPTVQLMTN","isNfc":false,"nfd":"1598b25d01a8fe35","nfc":"057f5ba8d7aa8021","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"28b6664c107d212d","nfc":"3e11a96cc23169f3","nfcEqual":false},{"in":"GSRVTVVMKVVRKRMLORRQTRLUMSQDMRKLVSQORTPONLNTVTQURTQLOOVNUQURMMUNVRKNMVTRVUQUNURROVTLVBPOQKKLN","isNfc":false,"nfd":"e5bf2caa0c9e358c","nfc":"a438d1d33210d761","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"9078d3998a3d9135","nfc":"f95263daf3d0321c","nfcEqual":false},{"in":"POTOKUUVSQMQRUUSRQLKMEMMRRUQNURGQVLQQUUUTMOMTLOLRONQFPVSNQVULQSR","isNfc":false,"nfd":"fe65d7f75c8442a4","nfc":"335231a049f74c35","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a063573773f5f334","nfc":"b43701e6ad065e8a","nfcEqual":false},{"in":"APLRUQQVLOVNSPETNMMTLTKUUBSKKNSITSLVKTOPQOPRJVUPOVRQSTHOKSTNNMLPQLTLHTLVLMLPKUNPFQNLLOUQMLOU","isNfc":false,"nfd":"b548d0db1ddab137","nfc":"0e8b8a32d76bcb44","nfcEqual":false},{"in":"IMMMMMMMMMMMMMMMMMMMMMMMMMMMMENNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"6933bf8cfe3de12a","nfc":"e87c2adc02837afe","nfcEqual":true},{"in":"EEMRQVVVHQQROVVNULSJVOLSUSVNQKENVKOMKURNPOPLQLKVLQQPOQLKSTSVVRRKVVLOLSRVTRNVKRMSNUDTPVTPNTVR","isNfc":false,"nfd":"8a49f56829d047a1","nfc":"44bfab57145d2b61","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b33de1f67dca3117","nfc":"8e66e4d526cdfc5d","nfcEqual":false},{"in":"VATVRUTOKNQSMOSTMSSKOMKSUQRMNRJSORLNLTRTMKKKUSMOVPPUSVLSQRKUV","isNfc":false,"nfd":"7b8bd85a5c4df83b","nfc":"137bd159df0b4efe","nfcEqual":false},{"in":"HMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"4fbce0f97eefbc0e","nfc":"4fbce0f97eefbc0e","nfcEqual":true},{"in":"FSULNTPQSOTVILNQSREOULMNNNSLOMRLK","isNfc":false,"nfd":"bbbfc94b21adb359","nfc":"8183e8a05ff2cbc3","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMEMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1af57a2d821f45c9","nfc":"2d8714abcd75997d","nfcEqual":false},{"in":"TOKSSKMSSVNQPPTTPPQONVVVMPLMMUOSLVURROOVPNMUOROUUTLSMTKPLPSKUKSRPUMFBMKKVMUOQPNSVSQVPVQKSRLVLVMPRR","isNfc":false,"nfd":"deea5a84733a6452","nfc":"deea5a84733a6452","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMMAMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"0fa14836915f6e53","nfc":"0fa14836915f6e53","nfcEqual":true},{"in":"KLNLVANABOSRSTKLOLUTQOLHQNUVURBAKOVPKSQOTDMRRUTRVVKKLRNUTDMKQUVUEQOINMHETUOQUJSPKNNKTMRPTTOSQNLRR","isNfc":false,"nfd":"f436ac262f42d491","nfc":"ffc5f3e40c440f3e","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMMMENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"c76313607490c1aa","nfc":"a8eb732af92075ad","nfcEqual":false},{"in":"DRNQPNMQUVNKUPQUGNMVUSTVEMERTTPQLROQMMVKTOKLUOLKTISLKVNPVUSS","isNfc":false,"nfd":"ccd5d25863e9718f","nfc":"d30ddd12ad905140","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"55070f56373b6930","nfc":"6be5ec242393fb57","nfcEqual":false},{"in":"LQKLKLLTUOUKNQMPRLKPTTKRKTNUMTM","isNfc":false,"nfd":"66826d8ac70b39e7","nfc":"66826d8ac70b39e7","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"20bf2c94fdd1136d","nfc":"018ae19cd84c1e5a","nfcEqual":false},{"in":"PSSSRQMKQQKLPSUQQSONOKNSNSOMOSVRLSMUNKVPRPJMMNPMNVKNRKKTRSGAPUTOTRUTSTQOVQROLKORNLNSMK","isNfc":false,"nfd":"ff2dd04ccdcc9001","nfc":"e64830a2057e3c29","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b39984a96d4a1a5c","nfc":"b39984a96d4a1a5c","nfcEqual":true},{"in":"DGLRSSTLTUSSNOKUTPSNRUTNINTSRVUKSOKSNUNSRTRVPKKVLKPPRSVKORLSP","isNfc":false,"nfd":"3ff4267842719692","nfc":"a45d24b7f24a975f","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"58cdd22d4a70d13b","nfc":"58cdd22d4a70d13b","nfcEqual":true},{"in":"AUKOOMLUVKOQVORUPVQVONEKSROKLNNPVRNMUTMKPKHMFNRTLTTNONQRKRPNPLKRKPKNKVTTUMCOSLMNROPUOQRRTSLRTRSSVNKQPVOR","isNfc":false,"nfd":"9bb5f76cc8ed3f03","nfc":"bf34673b3a77c612","nfcEqual":false},{"in":"FNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"713696b933a59d51","nfc":"713696b933a59d51","nfcEqual":true},{"in":"AIVPTQKAVNMROOSOVKQBSVPQQSOQVEKUTTMVPLQUKLTQSMLPRRUQOQOKULPLKTLRRRKT","isNfc":false,"nfd":"8c95c35dd6192049","nfc":"c3b7b3b50e1740e2","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"49a5e544ce1dc691","nfc":"dd76e17d9f1321b2","nfcEqual":true},{"in":"MRLQLNVUVOONRRTOVPSTLOTRRLTMOKLMVQNUMHULUSNLSLOKOTNMHROVOESOSMTPROLMRONRPOMSVKKQUUPFSTSRMUTOTNLSUQULPQLSQPRTQO","isNfc":false,"nfd":"a25e0d448d38b019","nfc":"2c3dae8d5f256269","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"afe7859a0f5d1875","nfc":"afe7859a0f5d1875","nfcEqual":false},{"in":"EEPHKVQLQUSSSLMCVOMKPSUKSTLPUMMRTUTLLMGQMTNSUMRVKLONMLPMSUOVVURVLLMKODBVRPTVTKTTPQASQSLUSMUKEUUONBRRPO","isNfc":false,"nfd":"31ed2556546b15e2","nfc":"aff8332a216d1043","nfcEqual":false},{"in":"JNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"68469f6d218e6f59","nfc":"60b5d6190217781b","nfcEqual":false},{"in":"LTOPTNKPVKTTSTVRSLTUUMUQUUQLSRUSMRVLSOUSMOLOQMTKSKTNKSKOTOPMREQUSVTKUMLUQKULVVOUKUOTKRVNVN","isNfc":false,"nfd":"601bc737739f0ec3","nfc":"c0fe3c6f74d814d1","nfcEqual":false},{"in":"GNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"9cb5ab628c8ab7f8","nfc":"9cb5ab628c8ab7f8","nfcEqual":true},{"in":"KLMKTSVPVDNPNTVSLPMUVQSPVMQPLSQ","isNfc":false,"nfd":"0b7fe7ff038fe834","nfc":"333f783f743f060e","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"281d1a51fd998d0e","nfc":"281d1a51fd998d0e","nfcEqual":true},{"in":"GONVSLRMNLNKKTNMTQUNOLQPVKURJUKUUNLSLCSIQQROKNPBMTOQTMQVRUKRR","isNfc":false,"nfd":"7d38fe3438c8083b","nfc":"3436222fa989c01a","nfcEqual":false},{"in":"BNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"2cac181d3f4fc794","nfc":"2cac181d3f4fc794","nfcEqual":false},{"in":"TKNUSVVNNPNNMKDOUSVVOMVQSMRNRBTPPVNULMOLONTQSVTLLMPNRSUMPPTPPMNPKKM","isNfc":false,"nfd":"34a7f24532e8016f","nfc":"0f1d62affef78915","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b6bef1dff6fa6a8b","nfc":"5736833f8893bd35","nfcEqual":true},{"in":"BQPVQMUUMOLLMTTVTLTNTLUVKUCLT","isNfc":false,"nfd":"98610b2fa7230598","nfc":"98610b2fa7230598","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b522d212719f824a","nfc":"87aa196559cf25b0","nfcEqual":true},{"in":"LBOQTRTONSKTMMTVKLLTNNMMNUOTRSLAKSVUTVEPNLTSVVKGPSVKLLOALSKNRUOTVGTP","isNfc":false,"nfd":"45a5c8fa92038ea5","nfc":"78596081626aabbe","nfcEqual":false},{"in":"BNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"93921c9a60b88084","nfc":"93921c9a60b88084","nfcEqual":true},{"in":"BLUUKONUVQLROMSNKLLRKSNKMPP","isNfc":false,"nfd":"5e3d92991d16b439","nfc":"fc1a5b60dc48122e","nfcEqual":false},{"in":"INNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMAMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b8ae2d86f0836e98","nfc":"844e552373fd5768","nfcEqual":false},{"in":"OHSOUVSKTOVONSLLSQRLTMUMVUOVUKLUOTUUPNSPOKUOUQRMRNPUROURVTMOOOVKFVQTNLKLNUTLNQUQSKSLVUTTNMNUTTGUON","isNfc":false,"nfd":"43e1a2ebaf4576d5","nfc":"43e1a2ebaf4576d5","nfcEqual":false},{"in":"ANNMMMMMMMMMMMMMMMMMMMMMMMMMMMENNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1257c15451766e77","nfc":"ef4b4e3be6239a25","nfcEqual":false},{"in":"FKVPOTNSVVONURQMTMRMVUQUTRPNTMJOTNKPSLNVRQRRQTKRNMSOLMVKVLVNQN","isNfc":false,"nfd":"c937c6351cfc698f","nfc":"a62aea4395b389ba","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"76df052d4cd93cd8","nfc":"9e25a2e9d6678aa2","nfcEqual":false},{"in":"QENOQKKRSUMSSUKMOUPSTSNQTFSNUMPLNSQRRNVLRSSOKMPQSLRKJMRRKPSNQBTSSTUQUKRLTS","isNfc":false,"nfd":"ac22cf0c026e492f","nfc":"fd4470b39d26b007","nfcEqual":false},{"in":"INNNMMMMMMMMMMMMMMMMMMMMMMMMMHNNNMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"abd64f7b936262f5","nfc":"e323f2f32a9c4698","nfcEqual":true},{"in":"MTKTOKRPRLQUOVQVEOVQSMLOVPRONLUNOKKQMVVUBKQLNUTKOTLUQSRMPHUTUM","isNfc":false,"nfd":"060c330ccbb923b9","nfc":"f741223c56633651","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMBNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"11e77f7f6ecde430","nfc":"11e77f7f6ecde430","nfcEqual":true},{"in":"GCSVUMUSSIMPQVVTMPONMUUNMRMRMIRNVVNMUVOVQPKNPVSPPUKKRPLUQSRNKVN","isNfc":false,"nfd":"49c1359a7446e18a","nfc":"f56a81cb0e20c0de","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"a4bba782dca07069","nfc":"a4bba782dca07069","nfcEqual":true},{"in":"KSLLNSMTSOPRTSRVPUTSKLKVUKRKURRTNMVPULQKVUMSRTUQKKNPVKQKMUSUUUPUNTVKGVT","isNfc":false,"nfd":"ee3a57dee8bab714","nfc":"ee3a57dee8bab714","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMMENNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4eb39f4f946716e0","nfc":"7e1bed9f8daced2b","nfcEqual":false},{"in":"MOKOKKQPLSQSNSNTVTUNNQPKSSUOHFUPSQKOLLPTKKOSKKRULRLPTPUUAKQPDEUSKVTRQUQSUSRTVTMLNPLPRSKSOKMPMUNSP","isNfc":false,"nfd":"b37ddac2bb63b1df","nfc":"94fa7f231e121602","nfcEqual":false},{"in":"DNNNMMMMMMMMMMMMMMMMMMMMMMMMMMJNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"9a4884d9b419c287","nfc":"ce89cc6ccee1f989","nfcEqual":false},{"in":"SQJLSSNKQNOUTQQTMUVNVMOTTMRPOLRKSOSUVP","isNfc":false,"nfd":"517c028e2213451d","nfc":"ff245ba09858ec26","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"171389f347bc1490","nfc":"171389f347bc1490","nfcEqual":false},{"in":"CFVQVROMMVOVKPKSKOQKNDKRNTMFMVNNSUTSLQTRVROLTREVKVNLVUOLSRUL","isNfc":false,"nfd":"b8642faa91a81901","nfc":"8bddc96b5e8a04dd","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"bbfab3a669c79365","nfc":"2d61055d1c6c81d6","nfcEqual":false},{"in":"UONLQUURPQMRTVVTNPUPUPNPMUTPMVVSPJUMRLRRMPRKLKTLPPRQAMTOLLKVRLKUKV","isNfc":false,"nfd":"2b792f2d5527163c","nfc":"db2341a2ee4d44b1","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"662350073e53b7ee","nfc":"662350073e53b7ee","nfcEqual":false},{"in":"JNQOPKSOHUVMLMSVNPPRONQLLMVSPVTGSNKNUKTTLMUQRRORVLMNKKPRVUVKUPOPTNQMVVSUOTPNSDVNUNTTPTTMSTPTOIQNOP","isNfc":false,"nfd":"6892645117d7608c","nfc":"f8c936ce7b93731a","nfcEqual":false},{"in":"HNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e2661d5d498e5965","nfc":"9e204ae55b73a3e7","nfcEqual":false},{"in":"FMPLKMULNREUQOPUQPTNKNFSMR","isNfc":false,"nfd":"7f6dad7098ed1dc2","nfc":"ad0bad90fa4fd2f8","nfcEqual":false},{"in":"FNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"51772489be280219","nfc":"51772489be280219","nfcEqual":false},{"in":"VVMTUSLTRKNVRRLKTSMPPRKVOPNPUTMOMMO","isNfc":false,"nfd":"f2a7059f556f94aa","nfc":"f2a7059f556f94aa","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMANMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4c9974f2a1090a5e","nfc":"4c9974f2a1090a5e","nfcEqual":false},{"in":"IQLVLNMNRULRPBONLKLPLMURNJDRVVMUORSLOPMVSMKNRTPUNLMNKTROUPK","isNfc":false,"nfd":"1a4d0672f227d1c8","nfc":"1a2a73a35463843e","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMMGNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"eeef4e1b4c5ab484","nfc":"eeef4e1b4c5ab484","nfcEqual":true},{"in":"ATVKNPNSUOVTNOTTVRQSUKOVP","isNfc":false,"nfd":"64c41de0b5d360a6","nfc":"64c41de0b5d360a6","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"381a5f0c5ad36707","nfc":"f490da986a889812","nfcEqual":false},{"in":"OEUCPUPOTNKUQTNTRNMRVUUOQOTMNNR","isNfc":false,"nfd":"c0f2495fe9cf206f","nfc":"1313e73d0d72d5d1","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"df5609c628db4af9","nfc":"dc0bc91c505c0dbd","nfcEqual":false},{"in":"HIKQMNKHKRPOQNRSSSRTTRLNFVNPVRNMKUSQLKMKUOPTVRSQVNTVLOTUTLKTRVPDMLRUMNOSVMNOVRLVSKNRMRPSQVKNLRLSUQNRSVQLQRMN","isNfc":false,"nfd":"9acdf9cf74b0c313","nfc":"1bc75441ff7e5729","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMMGNNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"38efd269b1345495","nfc":"8cfb4578f64735bc","nfcEqual":true},{"in":"DBLPUSPTQOUTNQVKNLGVVTVMKGRSMNSTLLMSUNSMQRTMSSKURQOMPRTLRUSVNVKORNQROKSPBSRVRLSQKKJSROVOV","isNfc":false,"nfd":"a89c49e6beea011e","nfc":"9f298e5529666ffd","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"8f0e2df1fa8c47ec","nfc":"8f0e2df1fa8c47ec","nfcEqual":true},{"in":"NMLJSPORPLPGMKKSOMOPKUSMQRRPQTVUOVKCVSNUQSVSMRUVOLVLRTU","isNfc":false,"nfd":"34ff2e83dd02165c","nfc":"7a313eba66d78c17","nfcEqual":false},{"in":"BNMMMMMMMMMMMMMMMMMMMMMMMMMMANMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"6cda8713b9719e29","nfc":"6cda8713b9719e29","nfcEqual":true},{"in":"AOOUUSTRNPKPVPVKPRMOLSMVKTVNKTMTPHVVRURLMRSPKTKPROLTKROONDU","isNfc":false,"nfd":"0b2f4f576fcd55a8","nfc":"94e95fa74d45eb97","nfcEqual":false},{"in":"ANNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e74ffea5684e6931","nfc":"e74ffea5684e6931","nfcEqual":false},{"in":"DLTTLTMSUSSKNKUOVMTVTQSMNPNLTMMOPQTVTDSKLLTUROUOVAMNMTRRNKNUQS","isNfc":false,"nfd":"2ac31e085369c948","nfc":"ef137bf72a7c2c90","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"ef6c32b7781c9089","nfc":"182e0a2430dd18d8","nfcEqual":true},{"in":"MVPLTVSUKKPVVKKLNKQSDNPUMVKTUMPPPRLLSKSVQLQFUKTQKSTTONTOLQTQQTPMOSLTSU","isNfc":false,"nfd":"b6d0010aa6457d71","nfc":"8b92f86137619e87","nfcEqual":false},{"in":"GNMMMMMMMMMMMMMMMMMMMMMMMMMBNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"47a16c0675d02c94","nfc":"47a16c0675d02c94","nfcEqual":false},{"in":"OOOOMQPLQKPTTPSOSQMSGKPKOTRTTQVQLOPLBUUOMOSSOVPMNRMOQUUVTROSOOOMLUSTUQUOPMQLQLVLULQKUPSSKMOQOVNQUOVRTQONN","isNfc":false,"nfd":"ec906d7c2f77b182","nfc":"ec906d7c2f77b182","nfcEqual":false},{"in":"BNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"808553493712f95f","nfc":"808553493712f95f","nfcEqual":true},{"in":"DMPOTVGPNTOPLOPQQRQURSKPPLVVVNPJSLOBJNLVQNUOVUTUVLVLVOVNKNVPMPMTTVNPQPTNST","isNfc":false,"nfd":"de6b86ca9abc751a","nfc":"acdc9c8575111c39","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"5b7a2fd976c40baf","nfc":"f30f09faf6b8853f","nfcEqual":false},{"in":"EUUOPOVNTSSRTKRLRKRTQLSPPVUVNOPVSOQNEMLOPVQTMCVMNPSQSPVTOPMQUKRR","isNfc":false,"nfd":"ee7809729898ef62","nfc":"4a8b9d8d6eb4f366","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMMDMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"fbde3338a2b83723","nfc":"1f7f23f698ebd171","nfcEqual":false},{"in":"EVVMLQPMQSLVTQNUPNLRLEQLRLSTQMMQOVTQRQQFBLLDUNPPCPTRKKOLMNVNQMSPRKTSMVNDMHKUKRNQOVLSKRRLTPSLTSOPQKS","isNfc":false,"nfd":"3ad9917beac08ecd","nfc":"7e70a1f882f08866","nfcEqual":false},{"in":"GMMMMMMMMMMMMMMMMMMMMMMMMMMMMINNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6b4d70cf24cfd892","nfc":"6c2b4a9d51719768","nfcEqual":false},{"in":"PUKKRKNUPLOVKENVRLRPVMRKRSQRMVMMECTMDNNTOLMSNLMKNRUPTLFRLMURRSKUVSSBKUVVCQVUSQUTOSNPMDONMVMNUQVSSELTTNQ","isNfc":false,"nfd":"6961e84624101205","nfc":"98f3547b08f1be5b","nfcEqual":false},{"in":"DNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0077656213c1d5d8","nfc":"e52d822f89bfa64a","nfcEqual":false},{"in":"KOMNUSRNTOTNPSSRKSNUKTVOTNMTLPUQHUBRRORTNOVVLOSMOOQSRKSOLKNMLTLKUUNROSKKNHMSQUVNOPVCTPTUV","isNfc":false,"nfd":"1a73b3c45b035a14","nfc":"cc37fce1a2c67f61","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMJMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"3937d812eb46e61e","nfc":"36c1a4c414ebb6b0","nfcEqual":true},{"in":"ATLQSMQVLROTQVVPQKUOSVUVLRPRSMTOP","isNfc":false,"nfd":"026f84543a33893c","nfc":"026f84543a33893c","nfcEqual":false},{"in":"FNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"adc17f4e98e85d13","nfc":"adc17f4e98e85d13","nfcEqual":false},{"in":"QNRROQNRONMMLPOMNOUSRLOKRRLK","isNfc":false,"nfd":"957f9ccfe207e6c5","nfc":"957f9ccfe207e6c5","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"34b9ff9207952c57","nfc":"34b9ff9207952c57","nfcEqual":false},{"in":"NPSPURMOSSTJKMUQVTQKNKKNFRRKPRRRUOPOTMTUESRTTMTMVRUMVPVMKUVLSRUQNSLSOSVAUVRDQVSNQKSLKVQPPSOPRTNOS","isNfc":false,"nfd":"04731d8e5b5f99a2","nfc":"fae863baeaf81354","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"e9767215b87b83bb","nfc":"dfcd79c0b0763955","nfcEqual":true},{"in":"CMTPSKMROPRQQTPOVQVLPKLBPQVOSUUVVK","isNfc":false,"nfd":"10974af81dd0a868","nfc":"10974af81dd0a868","nfcEqual":false},{"in":"FNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"79d72191b82ecb2d","nfc":"79d72191b82ecb2d","nfcEqual":false},{"in":"QPOVVRMKRTKQNTQMPMRVMOUONQSVPRPNJAQNUNRORNPNKSLMQSOLNQKNRORMIMQRPRQR","isNfc":false,"nfd":"8712ddf0a5a8a8dc","nfc":"e4030f7e76dc8ca7","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"662350073e53b7ee","nfc":"662350073e53b7ee","nfcEqual":false},{"in":"TPTRRNMPCSPOLPKNRUMSMRUN","isNfc":false,"nfd":"ba127841817a2787","nfc":"ba127841817a2787","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4393a5648951af3f","nfc":"bba67ee033345e64","nfcEqual":false},{"in":"MVVRKPSOUTNMNUKNOUMVOVDLNJNLOSRUORSSQUOSSVKUNQVMKRUPTQSQOLNNMRSVSOVRNOPTRAROTMUNLMLLSKP","isNfc":false,"nfd":"217d7d2bf08133ea","nfc":"51ff9874968ae9d8","nfcEqual":false},{"in":"CNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"cf01c3689e4f0502","nfc":"cf01c3689e4f0502","nfcEqual":true},{"in":"UVQOTTPVSKOSOKTPKUVCPKURSRHOQRQLLMQSQRLQNNNNRSNPQPQVNVKASMVQOOTNRRVVTO","isNfc":false,"nfd":"1c2b84f68ad48cbd","nfc":"68b9b2ec223e132d","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"a938b558187e5a4a","nfc":"a938b558187e5a4a","nfcEqual":true},{"in":"LVEVTRQRMMLMQPLUQTUUKORUURLTKTVMMAQTMJIMVUSPRKQSPVVMOTMMMUONLASRPSQVLQOONGSRSSRMNUKPLSSSFQTUSQRSQPLQVQT","isNfc":false,"nfd":"a9fed7a16907c08a","nfc":"05c29e148549b7b6","nfcEqual":false},{"in":"INNNMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"c6d0225485419d07","nfc":"29700b4ffeac3188","nfcEqual":true},{"in":"JKKPRRUKLLNPORQNMMUVSOUUSRRVQQ","isNfc":false,"nfd":"462876a81f55214c","nfc":"8809b274968c738a","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMINNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"38d1b21c07f0d5a3","nfc":"43411c768caa0caf","nfcEqual":false},{"in":"QUVVSRSVLUOMLKVKSUHVKMTSKMPQVVGKPMLNSNKNLBTTPNMNRSNNUENLTPNUNRPQLM","isNfc":false,"nfd":"7c4fc8704b3e3287","nfc":"0af7a44f1b5fa714","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"5d85745bba1008b4","nfc":"3ccb1bceac31914f","nfcEqual":false},{"in":"BVPNPMQKOUVNKKMPLFSSVLLULTPMVPNUMTPSKL","isNfc":false,"nfd":"33a24afaec14a15d","nfc":"33a24afaec14a15d","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ec42dfa967e8c4e2","nfc":"ec42dfa967e8c4e2","nfcEqual":false},{"in":"QLTOMRLPISLPOAKTSQSVVMKMLRPBOVCNLNQ","isNfc":false,"nfd":"5fb8387511bfc236","nfc":"db7d729253029fb1","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"790a5389d959e337","nfc":"790a5389d959e337","nfcEqual":false},{"in":"HLURLTPOQSAUSSVLPMKGTVRRUNULNNQPQSRRLNHSJNPKQQUNRMKMSNKVEPPNVTTQVUONPRNNTKMMMMOQKUQUVROTPVOST","isNfc":false,"nfd":"eb993696d0a7f032","nfc":"5972e1b0417f93c4","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"dd953d52f2c3f698","nfc":"dd953d52f2c3f698","nfcEqual":false},{"in":"UPMKNNQDMPMSLKTNSSUKRQRTUQ","isNfc":false,"nfd":"07f761f1fefa6d47","nfc":"d161b5d0470722cf","nfcEqual":false},{"in":"DNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMANMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"63dff5da48f65636","nfc":"5523749d7b1fb8d3","nfcEqual":false},{"in":"IIPONPUVQMULSKQLAQLOUKMNBPFNRNRTOSVNRVKRKDLJLKRPQVRU","isNfc":false,"nfd":"daef1b8449590a85","nfc":"a5fede91c96c16c6","nfcEqual":false},{"in":"FNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"053216266fbbb815","nfc":"053216266fbbb815","nfcEqual":true},{"in":"VUSQQPVRTQSSKMTSLQNVPVMUNTRTOUK","isNfc":false,"nfd":"f83870287c18099b","nfc":"f83870287c18099b","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ead2fe06880165d1","nfc":"94c01bb161eafb06","nfcEqual":false},{"in":"GNLOVATPUNUTVSPSQRRTLKVVQVQ","isNfc":false,"nfd":"fb583277ae41abdd","nfc":"fb583277ae41abdd","nfcEqual":false},{"in":"IMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0ef839f0b1f5f816","nfc":"0c61830c644ef266","nfcEqual":false},{"in":"IVPRVSVLTUSTTTKSTUMTSTTPKTSLQUQMLTULFKKMQQKSUROQVNTKEMKNLUVSSN","isNfc":false,"nfd":"2bd7996878e2d00e","nfc":"c8de28feb9ddd198","nfcEqual":false},{"in":"CNNNMMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"883802076672ba53","nfc":"883802076672ba53","nfcEqual":true},{"in":"RPUNNKQPKRLUSSRRNVONRQRVRNVKSVKGPMPKKRUSSQMVPPTRVSKTSPTNKUUKVSM","isNfc":false,"nfd":"57b3e87d298b74a1","nfc":"57b3e87d298b74a1","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a7e8477ca7cd60f0","nfc":"a7e8477ca7cd60f0","nfcEqual":false},{"in":"OPPNTRPRNOVUKMIVRLRLARVKLTUSONMUMUPMTRJHLPKUTVKTRMPOMQNTKVLVQKKPQTSQOVVONINNTBPRKRTQASQURKRRQFOSEULPPVOKS","isNfc":false,"nfd":"1db30b0909736ec0","nfc":"6344bd401dfbd18e","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"9517344174a65fd4","nfc":"9517344174a65fd4","nfcEqual":false},{"in":"RGKQSJROKMVRNQMLQMPVSVPMQSRTOOORNSLMSKSLOOTVRQROQLQPLPQRRQSUOUPSMQMTVVQPLQURGKPKHLKTRROEOFU","isNfc":false,"nfd":"c75187ff130f54ca","nfc":"8fa72afc2df6ced3","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"041f876967794cb3","nfc":"766d420d75563253","nfcEqual":true},{"in":"ULSOKRINMTOVSRUUUUOQLPVSKSRMSLKQVLVP","isNfc":false,"nfd":"1ef9fac91d2af1f5","nfc":"9a55e32c28c4c74a","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"8bc924fc910b3f69","nfc":"34b3f0489bae3636","nfcEqual":false},{"in":"RVNKMMVNMUKNLQRMVRUUCQMP","isNfc":false,"nfd":"5e321fded1bd8bab","nfc":"5e321fded1bd8bab","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMAMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"625294472b87edb0","nfc":"705b2ed5bfad4870","nfcEqual":false},{"in":"UPUQMTNMNKNTDLLUKKSROSVMKKUNUUVVKUTLSBJPLMKMUPOSORVMQTVVPPNMKKVRLRUTHQMQRUPRKUUSVVUOSNRQVSRUUOTNVQMS","isNfc":false,"nfd":"bf2b3bb17c087ef0","nfc":"9c413ed2eda37434","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMJNNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"157c6955c459038a","nfc":"ff5962b6496040c2","nfcEqual":false},{"in":"CTLTPOUTKPROOLOUKVPRRSLPOLOOEVQLSOKNLVRNQLLUQKMVONTMKQUKPAUPRDQNMKNQRSH","isNfc":false,"nfd":"c4917d4f0a8aaf53","nfc":"b95232f62e88d02b","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"94f9bf1c81e2b749","nfc":"4c3511479cd2b060","nfcEqual":true},{"in":"JPNTRMTOLUVRVLRTTQMRLLUQVFPKKMTVVVUN","isNfc":false,"nfd":"1f9b36bec561839a","nfc":"c2e1f585d093a2cb","nfcEqual":false},{"in":"INNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"2ee8d3297fa0d8a3","nfc":"445525c3506fb202","nfcEqual":false},{"in":"TTTPRRKSSMTOPTKQNSVSMVNNOPRPHNNVOKIIERLUVLLIUNNKQQOVRUOPJRPKOOQUROMNVTOKUOVLNATPVNTRASIOPPKQKNSOQPMKOOVVPPSQT","isNfc":false,"nfd":"6fd3ddaee7f04d7f","nfc":"5606c54cb337d04b","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6d93fa7e87e28633","nfc":"6d93fa7e87e28633","nfcEqual":false},{"in":"TEBVPNRUMRSMKUMTOOULKKVLONOSOKPPOTFGOOPSMORKSQETROULOCPVOTSU","isNfc":false,"nfd":"922f1ce8b69e7f6a","nfc":"91df6d0dcfae6a91","nfcEqual":false},{"in":"GNNNMMMMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"707e8008d5c41cac","nfc":"707e8008d5c41cac","nfcEqual":true},{"in":"HTMNOULUQDVUVRTMQTLVUOUDTSJPQMNLQMSQP","isNfc":false,"nfd":"7c446a8e39453c11","nfc":"5513cd1c0c4fd568","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b517badaf6cc7abb","nfc":"96b2e42fab531b3f","nfcEqual":false},{"in":"LVVTOOSNPSUOPKQMKQVKVLANOQLRERTNVNMOVTKUORURRUQPQKNOKLNPSMRTTHUUQPNSTPMMUKROPEOVQTVSSUKUSPPRPSSS","isNfc":false,"nfd":"b1b93227813a4716","nfc":"0cb55fe9e1caf534","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"15e572660f0b5752","nfc":"15e572660f0b5752","nfcEqual":false},{"in":"MVTNMOQURRRMPCTMQSPLQKMAC","isNfc":false,"nfd":"7631fe96239d6550","nfc":"7631fe96239d6550","nfcEqual":false},{"in":"GMMMMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"07f27d219aa3d568","nfc":"07f27d219aa3d568","nfcEqual":true},{"in":"QOSTPRBLKLTBRULRKNJQNLKVOLF","isNfc":false,"nfd":"49c2b30c2098e9ef","nfc":"f69a2778bf618aa2","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMMFNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"292bfd57dcf2ca8e","nfc":"292bfd57dcf2ca8e","nfcEqual":false},{"in":"CMLSSVUPKQRVOVKSQTOSRTUROPEORTOVVNVVMU","isNfc":false,"nfd":"7f73a5321f24843c","nfc":"41bfcbc8c6b9e376","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"23826ffa20e729ee","nfc":"23826ffa20e729ee","nfcEqual":false},{"in":"ONTNPMVUNQRQKVMQSKQOLMKPKQLPPLKUKOORULMRUVDPPUOLSTVKPEMQO","isNfc":false,"nfd":"585d6ed4ca7d2015","nfc":"596ce9617e820ce2","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"8bbba736aa9eb5e6","nfc":"8bbba736aa9eb5e6","nfcEqual":true},{"in":"KJQKVPRMRONQKRQVQRVTPOOUTQOKPQUOKUOPVKVRNPGMRPTMSVQOTKSOQPPTHSRKNKNLMKRQUKVQVTNOULMRSMUMONKKTQIKQ","isNfc":false,"nfd":"3bebede0317b59e7","nfc":"801f152619bb725a","nfcEqual":false},{"in":"INNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b0adf0a0cfe54713","nfc":"5a39b81ec1c4c9fa","nfcEqual":false},{"in":"NOKVVTTTMMSKPVTVLPLNPOMOR","isNfc":false,"nfd":"85c871650c08cf36","nfc":"85c871650c08cf36","nfcEqual":false},{"in":"DNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"eb389a7817fd70c3","nfc":"89f60799a472c3a6","nfcEqual":false},{"in":"RMKUNJLRVKKNOLNMLORVRRTRNRMBPKLTRKRTNHRSPRMVBROQNQPOMT","isNfc":false,"nfd":"08c7c77ff355bd91","nfc":"a979b4c324542ca2","nfcEqual":false},{"in":"FNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"62b6328fa8619b06","nfc":"62b6328fa8619b06","nfcEqual":false},{"in":"ELKGQOLOUMMTRPPNVOKNSOPOLO","isNfc":false,"nfd":"a2aca56c556775b3","nfc":"1775a52c927f2405","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1eca046a82ac28b9","nfc":"1eca046a82ac28b9","nfcEqual":false},{"in":"GMRSNUQTMVUQQOQLSOQOSPSVVOUUPAVANPNMQLUTNLRLOOPTLTSQPNL","isNfc":false,"nfd":"d6ba3d86aadfd449","nfc":"d6ba3d86aadfd449","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b3b935500ee0726d","nfc":"469c0e2337c40e9f","nfcEqual":false},{"in":"CVTNLKLUROPQKKKVSMVTSMQSQCNULROMMPRVQQMVRUOPQNLVTLURKNLVVNOQTRLPNNSRVPRORQSPKTMPNKRVQOUKQRSALSPQQSBRKTQOO","isNfc":false,"nfd":"2152613163eddcb7","nfc":"bb1b7246799636b9","nfcEqual":false},{"in":"ANNMMMMMMMMMMMMMMMMMMMMMMMMMMMAMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"5ec598954bf1d848","nfc":"5ec598954bf1d848","nfcEqual":false},{"in":"KRTRLSRURKAKLPKUPSKPLUVQV","isNfc":false,"nfd":"2afbf27df1810b96","nfc":"172a4e2ecb0d1a35","nfcEqual":false},{"in":"DNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4687945b8484e187","nfc":"d4200fbd323de50c","nfcEqual":false},{"in":"RTDTQQVTSTKRTLRMTTKVNOLOSUDMLOQVQQMOSRKNQKTPVKNTONMKQLMPBRLT","isNfc":false,"nfd":"2284061b394907a4","nfc":"a7e0cd7bad2f8b7b","nfcEqual":false},{"in":"DNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"c04ccd7e275f13ac","nfc":"11935fe7df558b8d","nfcEqual":true},{"in":"OONTPQPQQSUOALLKVOPSTLPTRKQ","isNfc":false,"nfd":"068937d4a17325b8","nfc":"df1a337e7a415d09","nfcEqual":false},{"in":"INNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ad8c42af6785fd52","nfc":"760673ca4826be81","nfcEqual":false},{"in":"OKMPUUVOSVOKLPMTRVNTSOULMVMOTEOKPPQQOTTRUQTNMOSRKJVLKUQNLRMVR","isNfc":false,"nfd":"6b927f7518956c2c","nfc":"a1740931f9a179b6","nfcEqual":false},{"in":"FNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMJNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"f63d294ec2da7097","nfc":"c8228d0fcc685307","nfcEqual":false},{"in":"KLKVUQVURVTKPOLOPPSELPRPMNSURORLMQKGAQRNRNNVSNQVVKPLQQNMUKQSUQQVTMK","isNfc":false,"nfd":"658641df48d983c6","nfc":"012f244200e0a536","nfcEqual":false},{"in":"ENNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"597d27cae39bd4ae","nfc":"bf28cfa7c2536d41","nfcEqual":false},{"in":"GPNPMOSNKKSMTNDTQKKRQUQQSUQINQV","isNfc":false,"nfd":"1203832a2085e533","nfc":"72c1b51f6af310a5","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"bd724df99e816dc0","nfc":"072bff1f91a8bbd3","nfcEqual":false},{"in":"SNTSOTTKOVLAEPUNNOMLUKNUPRROLQPRUQTSRSVTQSKQOLJRQTUOKVTPLQVQLVLUVUTMKMQKVUOPLNOONNVISOTRMQTTKNVNSSS","isNfc":false,"nfd":"4070ad8cfbd3ddc8","nfc":"1783a5bc8ca67b45","nfcEqual":false},{"in":"FNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"85033aece0af664d","nfc":"85033aece0af664d","nfcEqual":true},{"in":"PLQSSVLIROLSTRVTSTKRSQKKMLQVTNNMSSOLPKUOQQLNUQOSMNRRIV","isNfc":false,"nfd":"2e0c48a579866d8b","nfc":"01f0ebbf267cae89","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMINNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"ccfa4f1200967866","nfc":"a6afc3b0382f8a1d","nfcEqual":false},{"in":"TIMBPNCMANOSQOMUPTLTNKVORMRLSUCKPUPMQNVACMQKPVFQFLKMLO","isNfc":false,"nfd":"042a136f2adf7d30","nfc":"e0a75c348ccb6c38","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"7badfd00d176a6a5","nfc":"a7d6a25df6c665d3","nfcEqual":false},{"in":"VNQKSKNPMVVRNOOLUPUSNNLKORNGRKMMLUNRKLVQSNOVNKUKONVLOSMQSMSMRSTTL","isNfc":false,"nfd":"7b172aead3e0078d","nfc":"7b172aead3e0078d","nfcEqual":false},{"in":"FNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMJNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"c3937fd1c7516c01","nfc":"1ea1699880015879","nfcEqual":true},{"in":"IUVTLLQVTQNNNRPOPQPQTMVSLVLNMMMTVVSELNTPSTVOPTNUPRTMUSNUSSMNSSLQUMKOQLLS","isNfc":false,"nfd":"3e0c8d146edf38a3","nfc":"c5853e9ea09732fd","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMANNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4cbfdd70522ccfcc","nfc":"6557e19bf892ff68","nfcEqual":false},{"in":"MLRUONMKTSRUQUQITLOQKOTVUMTLMKVUPONPOTTTTQVPLLQPN","isNfc":false,"nfd":"1f520c85e30b1ead","nfc":"903c93801d51ad54","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"886de1673e94471e","nfc":"886de1673e94471e","nfcEqual":false},{"in":"LOTLMKQVKQTPMNLQBESPKPPNMLLVUVULOLUMHRTNKTLSTKSSSVLNRPTMMRVITQVRQEMITVSSTLDVVVSATTUOLVUMONOMRKTNFUNOOQL","isNfc":false,"nfd":"908ce4609941331d","nfc":"b0db67a1f2321585","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMMMMFMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0cf30c67d01e3fc4","nfc":"0cf30c67d01e3fc4","nfcEqual":false},{"in":"UUROKVMMMPUKRVRLVVULNVTSVVPUQSORVUKOTUVQOPSRDLROPNTTPRUUMVQMVNOJPVOQOVKPSKLPTNQTMOUNSUNNVIPUT","isNfc":false,"nfd":"0cb8087ca012b6a0","nfc":"e00ff05b9d5423cf","nfcEqual":false},{"in":"GNMMMMMMMMMMMMMMMMMMMMMMMMMMMMENNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"112405c40a0e34c5","nfc":"275c346dbe5c4d33","nfcEqual":false},{"in":"UNTRKREVTVMUOTOQKRLNVOBUOQPIPKNMRVVQVNUNRVOQVSQODSUSQFUVMMVOQMNNSSKVILNLRLARVVVGKPLP","isNfc":false,"nfd":"5a1a9e0b825e4353","nfc":"5518e62ec7269961","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"f33d9060a7abe71c","nfc":"f33d9060a7abe71c","nfcEqual":true},{"in":"PDKKSPLOOKQOQQOUVNVTRKKTLJKTVQ","isNfc":false,"nfd":"79f206a6e2ec7632","nfc":"f6894daf79fdb8ba","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1968f829d7c67934","nfc":"cc5c9be63c0eac6f","nfcEqual":false},{"in":"KPTSMLSKEOSPMLRRSOUOLLLPSJRKQPOLMKNOM","isNfc":false,"nfd":"e86750c95e62b678","nfc":"be6b0f163000ece8","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"342e9eb841eff77e","nfc":"342e9eb841eff77e","nfcEqual":true},{"in":"HOMTNQMSULVVRKSNSMSLKMPQSLROUOSOQPSKEMNTUTRKLRROQROMLUPGOSRVQVSUUNKLURQMKMNSQPVUTQRQVTSQOPPOMNPPNVRSLS","isNfc":false,"nfd":"c093f7350483fb69","nfc":"799d6766f3bbbe32","nfcEqual":false},{"in":"GNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"e4dfd1029bb781a2","nfc":"e4dfd1029bb781a2","nfcEqual":true},{"in":"TVALUPFLUKPVVNQLTQQVPPUOTRRSLUNUKNLPNPKKMRPTLOKOMRSRNDTPOVKLTSKNQUMPLOMKQMSOAVVTOUCNOPUMPNMRMQIT","isNfc":false,"nfd":"7542998fe4091438","nfc":"8af996cbd2bd3bfe","nfcEqual":false},{"in":"BNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"60d5ea8617c19739","nfc":"9da99ba5cb6456e0","nfcEqual":false},{"in":"JONOLNOUVRLPSKUVSVVMQQLOPPUNRQSSSIVU","isNfc":false,"nfd":"0d5181d2e69cd096","nfc":"544e3562ce3d2fe6","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMMMMEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0a3fa655df855f45","nfc":"30f9d3f043076692","nfcEqual":false},{"in":"OLTMPQSTTRLHRMKROUTRKPTQTUTVVVNPTMKTVQKQUTOHVUPKUKQQTPLPLPVRQLMOKRORT","isNfc":false,"nfd":"f288c2ca84f52b59","nfc":"f288c2ca84f52b59","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"0a3c4ea58de46749","nfc":"0a3c4ea58de46749","nfcEqual":true},{"in":"OQQOOOTRTKQSLMURTLSNPUVSPQRDDV","isNfc":false,"nfd":"75db92efe3ebf4c4","nfc":"c5bae7281e025c95","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHNNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"daec2287ef4b4207","nfc":"daec2287ef4b4207","nfcEqual":false},{"in":"UPALTMTSMKPOKRUVKTQOMOSUQNVTSKHKRRSLNKNLTMNTTTMKSUINSKQMKUTNNRUORKLQMVPS","isNfc":false,"nfd":"0bd90c3929b88e93","nfc":"3574ced715fc8801","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"8f0e2df1fa8c47ec","nfc":"8f0e2df1fa8c47ec","nfcEqual":true},{"in":"JTEQPTTKNUKULMKPPVVEOQNMROTQSUQUSRVNRRN","isNfc":false,"nfd":"f7214f0d7d345e5b","nfc":"58cc280041cab80b","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMINNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"47cb1b73ce64ed06","nfc":"08fb3d55222c2507","nfcEqual":false},{"in":"SOOUVVRSONKVGROOMTPQMTQPL","isNfc":false,"nfd":"12543e3cf66e26ab","nfc":"12543e3cf66e26ab","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4df9ad13b328438b","nfc":"4df9ad13b328438b","nfcEqual":false},{"in":"VTIUOVULNQPTOTMVOPMSUKRNOUMTUPKUSMTINSQUNLMLURVKQNPKUNQQPUKNOLUM","isNfc":false,"nfd":"a9e48fe6831ad6cc","nfc":"51ab9a2c5450e10e","nfcEqual":false},{"in":"DNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a06f73ef000dbee5","nfc":"af228163d1fc0cda","nfcEqual":false},{"in":"TNVUKQLVMQRRMRNSQKSTPQKMPSSLOMQMGPMUNMDMOTUNTMPNLRNMLKPRNLKUPLSLL","isNfc":false,"nfd":"aaa6cde39e49181a","nfc":"efe5abd87a182a91","nfcEqual":false},{"in":"GMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b182aa5b474352ba","nfc":"b182aa5b474352ba","nfcEqual":true},{"in":"SUKKQVLTOTVMUVVUVOKLPQTTQNPKULORSLLMTUNRSOUUMLRSNCHUSTRSTPUVQTUVPMQRSOOKQQQVLSNNNKUTLQSPQQMRNPUKMERSLROQLPMN","isNfc":false,"nfd":"f3dee42be769c938","nfc":"f5f2b3c6a5eca35a","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b33de1f67dca3117","nfc":"8e66e4d526cdfc5d","nfcEqual":false},{"in":"SKNVUMSQNKTPMSNQNSNMMPPQPQTKMLKOFDQQVMOCTKMGJKLOVQVMQPDVPNDULTVTNRPVLQVPPPTKRNTQTKKPRTVSURPSVS","isNfc":false,"nfd":"968b829c5e59e8b0","nfc":"84ebf809d6ae9250","nfcEqual":false},{"in":"GNMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"a8c02ebcc5eb0f63","nfc":"af887d27cefc2816","nfcEqual":true},{"in":"IHMSPUVRUNQSBVKLKKNLRSOKNRUOQNRKPTVTKUUKSRUMOUQMUSUUDQTKMOULLTTKROPMSSOTROUQLRMMSPUNBPMSQ","isNfc":false,"nfd":"92484bd5693cca4e","nfc":"afc0c4b03174df65","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"d872d258407b14e7","nfc":"b9c159d577e6919f","nfcEqual":false},{"in":"NLDNORQKRUSRQTVOMQQPFVPPMUFTNVRVQPMSUUPDUSSTMMNNVUSRVFSQLVUMVKRONKUVTMMQO","isNfc":false,"nfd":"c7734ef76251b43b","nfc":"2aaddda23e547769","nfcEqual":false},{"in":"GNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b2e8b331f0bcb9dc","nfc":"b2e8b331f0bcb9dc","nfcEqual":true},{"in":"ECKURSLLSOUQQBVNKRUTMMTSLHLESIMMVTNOVQ","isNfc":false,"nfd":"0c125047e35a42d6","nfc":"174095281a759b6b","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"eab8d65c287c47ce","nfc":"eab8d65c287c47ce","nfcEqual":true},{"in":"DMLMUUTKUPUOLSQNSOOLSSVKQKVJQPHPNVKMRQSLNRPSVMMUNKNQUSSVLRVUUMVNSPQTVOLMLNKNNOMLTQNVNUSSTRTUQ","isNfc":false,"nfd":"3a449c59c90800f8","nfc":"c52e5ec908d1437d","nfcEqual":false},{"in":"JNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"d8ac0e9fb551b8bd","nfc":"a751bb9d55daa3f1","nfcEqual":false},{"in":"IVSRSPORPTQRVRMUOQLPMNRVSTVOTVLNVRTKOSUUSNSKOLLQQFVUOLQKMTVUOOMQURSVSNPUTONKNLTRKQKONPOPKOQDK","isNfc":false,"nfd":"73c329d650ecf932","nfc":"c5da4cfd3e9d19b3","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0fb34ba806c8aa5a","nfc":"0fb34ba806c8aa5a","nfcEqual":false},{"in":"KRNQDSKPMJFLLOUUKRNSPQLLLOQPSPTQGTULPTBTPOQQTKPVSUVLNSKPTKLVUOTGGVKSSOULVOTNSANPKOSRNUSORK","isNfc":false,"nfd":"1c4c0a917e32b3b8","nfc":"d0549416011c3aa7","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMMMFMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"7c962ebcc554435b","nfc":"fcb5c9e38085e7bc","nfcEqual":true},{"in":"JQSKLVTTRTVSTMKOKRPQOQTDMULMQMLMON","isNfc":false,"nfd":"7e48164add75162a","nfc":"28518f7217143a19","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMINMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"709a7471888b7a10","nfc":"a27581597a65370d","nfcEqual":false},{"in":"AMORUSLTPRQSLLVMUPMOMTSVMKORTSONQQLNLNVRBDTVTSKNKSVKLNNKPRPSRNPMLRMALMNMKTOOSPRKLSOOLUTLOSKTMVMO","isNfc":false,"nfd":"a525c0030aa4364b","nfc":"746d719bfd6c635c","nfcEqual":false},{"in":"INNNMMMMMMMMMMMMMMMMMMMMMMMMMAMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"40f3c4026a83fef1","nfc":"fdd691d8ca9ebc03","nfcEqual":false},{"in":"UOKMQMPQNUQNTTSQONVSOUPLVQTTRQSVUKSUIHNUKPSPLUQORMUUNFSOBTANSLUTSRQKMLR","isNfc":false,"nfd":"c3a96baac48be918","nfc":"69696b77b1ee19f1","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMHNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"9867782639248d72","nfc":"a60311ad12d91a74","nfcEqual":true},{"in":"CMVPTRNOPUPLSRPSMOVSRTSROAVLPMUSSSLQUVRNURQPUMNNOOOVOVVMSTNQDKN","isNfc":false,"nfd":"436721ab6ee62cf4","nfc":"b670ac68f4ab7e8e","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMMENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0040f44c7bd56665","nfc":"cb612c1b30f15046","nfcEqual":false},{"in":"TQKPNPUTMSTVKQRVRPMMTPNTMTNROPOPNSNJN","isNfc":false,"nfd":"af95800e790021da","nfc":"3c58334e007330d2","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"afe7859a0f5d1875","nfc":"afe7859a0f5d1875","nfcEqual":false},{"in":"GVMVPURQUNKVAVSKUMLVPSTMMOQKTQNLRURQMKURVRUVRRQOMRPMN","isNfc":false,"nfd":"7df865207f49fd8a","nfc":"7df865207f49fd8a","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMMEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0b5aac34ae842b08","nfc":"cfe0e968f17d408b","nfcEqual":false},{"in":"VVUUTTRMVTTUMPIMUKLKVTRVSLOLUSRPVO","isNfc":false,"nfd":"9855268856b22e4b","nfc":"8e1d7203ce156dda","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"142a120c87817c83","nfc":"142a120c87817c83","nfcEqual":false},{"in":"IPPPRNRLNVVNTLMTONJBQOLPST","isNfc":false,"nfd":"91140a4d27bc4a2f","nfc":"cfe70b97728e49eb","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMHNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"81f60c84545e7fe3","nfc":"3dbaaaff61102024","nfcEqual":false},{"in":"QLSKKSTRLVMRMUQNMNMRVTTMROQMTLPSLSUVSKSNVMQUNETNNTLQOKSUMRPHMKRPTPSONNVTVUSNQVTTPPSV","isNfc":false,"nfd":"e74a10ffc52877b4","nfc":"e5127d5f38122386","nfcEqual":false},{"in":"ANNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"dfa6efe56aa49848","nfc":"dfa6efe56aa49848","nfcEqual":true},{"in":"VLFNOTNRQAUVSOVTOPLLLTUOSTVMMPMONLVVLQMKVUKPTVRPVQNSPAPQKOPMPMUTRUNOQ","isNfc":false,"nfd":"2aa3c5acff248ee5","nfc":"c6fd004903f1f288","nfcEqual":false},{"in":"DMMMMMMMMMMMMMMMMMMMMMMMMMMMGNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"16f7886b2d474910","nfc":"2e96c376f2fc702c","nfcEqual":false},{"in":"BJVQOSQSSSMPRQLMOVLKRRMURNRDMPOVKPULSMVURKPMSPRKKUVMOTLPNOMLMUSSKUMKKIUVOLVPRNSKKPPSMNRRORPQKTQ","isNfc":false,"nfd":"38055cde3be705f0","nfc":"ec0773edf7ae78f0","nfcEqual":false},{"in":"GMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"bccfcf0774c2b364","nfc":"bccfcf0774c2b364","nfcEqual":false},{"in":"POKPKNNQVOOGNRMPVMSMSQOIUNKIUP","isNfc":false,"nfd":"7de995a59ea08b63","nfc":"99b15b6163936215","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMENMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6ea6861dc37176fc","nfc":"b8a36d9e11a37620","nfcEqual":false},{"in":"IPKORPVPSPORPVPPNLLNPKKURMSNPQQKRROVUKVNVPTTNPSQNSNHRPTMN","isNfc":false,"nfd":"3cf6dfa0af04b4cb","nfc":"8d0625a68054d0f8","nfcEqual":false},{"in":"JNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"fafdf7784a3ad038","nfc":"331129e452f8a0df","nfcEqual":false},{"in":"UVPSULLNQMFOMNVMNPKVKORJSKKPNLONTKLMPVMONOTGSTMLKPPLSSQNKTMMVNVUMLKKJDNNUQVVKMQLNPPMMQSVKVSRKSSUSLTON","isNfc":false,"nfd":"0811a3bfe4224367","nfc":"1c470c807df49825","nfcEqual":false},{"in":"ENNMMMMMMMMMMMMMMMMMMMMMMMMMMMINNNMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6747aefd45b49b89","nfc":"ea151a03c10f2507","nfcEqual":false},{"in":"FNUSQKKKSRKUQQPQKNUTQLBTVLOSUQO","isNfc":false,"nfd":"451a14db66d4cf08","nfc":"451a14db66d4cf08","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"8bbba736aa9eb5e6","nfc":"8bbba736aa9eb5e6","nfcEqual":true},{"in":"QKTLRTNMMKKQKONPSMMROOTOMMNUOTUTKUUJHOUTVMPSNNPSSCMNSQNLQLLTMKOTVPLTTPTCGTNQQVBKFVNLOOSSMNPOVTNDUUS","isNfc":false,"nfd":"193d870171415779","nfc":"f4aba0a55165b233","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"df5609c628db4af9","nfc":"dc0bc91c505c0dbd","nfcEqual":false},{"in":"HMPUUQOVHTQRKVRMKKCONTKQMUKRSRTKLKSSKMQRSVVTLUPRTOTMVLVURLROMLKSM","isNfc":false,"nfd":"cc26efc3a421619d","nfc":"99f4b17781fdaa8d","nfcEqual":false},{"in":"GMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"160a798f9f9ff696","nfc":"160a798f9f9ff696","nfcEqual":true},{"in":"AFURPPOTLVNTLNRMTHPNMPGPTUVFSQMQLPNLPIRVTVPTQVNKTKLROOMLVRKPKQKRUNUPVDLSLRLLJKPRGMNVLTURVIKROMVPUPQERNMMNQ","isNfc":false,"nfd":"bc55c9cc3ba71452","nfc":"7e033bdca92592aa","nfcEqual":false},{"in":"ENNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMINMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b0e059ab6782c1ee","nfc":"23cc6013ada715b5","nfcEqual":false},{"in":"GASLMUSMRSKMQMQRLQROPUMPMMQTHSVNUSOVSMKVQVMRUNMKMERRTQKOTQPPRPSPQTOOUSLPMTKTMNNNLKMLSLQV","isNfc":false,"nfd":"9c0ebc07572bbe96","nfc":"406f205301850b7e","nfcEqual":false},{"in":"ANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"662350073e53b7ee","nfc":"662350073e53b7ee","nfcEqual":false},{"in":"CTSSVOOVSUTPQLQRVPNLTLTTKKLPNKSLUNTRNVROVQOPBPKVUOMSQLNTRLUKRPPVNPNT","isNfc":false,"nfd":"9c77cf2de20f6d9b","nfc":"e0b216ac0feeb3fe","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"3a7994e55c79af4c","nfc":"66ea49e1d3667a0d","nfcEqual":false},{"in":"UTQOOVSOMROOUNLULUPOUSMMUVRLMQVNRRNVSKPDOTUNSPQKULUVUMUUNTUTUVPKQSPOUTMLPRPMULUSM","isNfc":false,"nfd":"c6c87909af9d0e31","nfc":"b7c1381226afdb9f","nfcEqual":false},{"in":"CNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"b41208c9c651a1b6","nfc":"b41208c9c651a1b6","nfcEqual":true},{"in":"GLKKRKKTUPOSLRNVLNRTTSLKLOLOVMNKVOUSNMSNRKKRLPSPKKRRNRERNKMTLQRMNMNRPR","isNfc":false,"nfd":"518bda7d808524d7","nfc":"520df8149acd982b","nfcEqual":false},{"in":"HNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"5a2d7b016690af1a","nfc":"5a2d7b016690af1a","nfcEqual":false},{"in":"GSPQTVPRNNLLTOSSIRQLMLMQQKQQQQTRMLNLSNMUMTVUVMOOLUKSMOOSSKOSLSGK","isNfc":false,"nfd":"b05f4ef5e24e2582","nfc":"2cd338214eabc560","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"4393a5648951af3f","nfc":"bba67ee033345e64","nfcEqual":false},{"in":"IGTKUSOLOVPNMMONTNKVSNQVQSOVOUROUBRLOPMVUOONQNQKUVNRRLROKPVPQQMUETMQL","isNfc":false,"nfd":"2f2c605a9cdeb51e","nfc":"b631c8636365df34","nfcEqual":false},{"in":"BNNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"4f44eb06cc87b97b","nfc":"4f44eb06cc87b97b","nfcEqual":true},{"in":"NTMPUOLLSPQSKVVVLVKUQMPBKRBQRKTRPBOSRRURVRNQKVMNMGSQKRPMQSNLOKKHNPPV","isNfc":false,"nfd":"ef166281bc458640","nfc":"03d7c41c0a230425","nfcEqual":false},{"in":"ENNMMMMMMMMMMMMMMMMMMMMMMMMMMHNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"895f2444f3e7c037","nfc":"8b5bb94c02ece6ae","nfcEqual":false},{"in":"SALRSKQNONOVSTONLTLLAPMORSKOUUVORKLUQPNTHUUNUQPQTOTTPQQOOPKRLLSSRQMNCNKRULOTPOORLUQUNPVPPRLPL","isNfc":false,"nfd":"9655fe6387a19379","nfc":"534971cc94f65ae9","nfcEqual":false},{"in":"CNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"89434e31c5903067","nfc":"89434e31c5903067","nfcEqual":true},{"in":"NUCSUTMOPOOQVNLKOPRTVNKTNUKVVRMORVVOUIKVLTRUMKSTNSTNKMOPQ","isNfc":false,"nfd":"6c59fbc58ca853b5","nfc":"79f783ae8ff1689a","nfcEqual":false},{"in":"FNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a786bc9ae9177d54","nfc":"a786bc9ae9177d54","nfcEqual":false},{"in":"FPKUUMPTTOSUQUTQQRFGLLRPVPKKRVKQ","isNfc":false,"nfd":"068713bc1e40825a","nfc":"068713bc1e40825a","nfcEqual":false},{"in":"DNNNMMMMMMMMMMMMMMMMMMMMMMMMCNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"251da33f10d7dccc","nfc":"bc2632c44e311361","nfcEqual":true},{"in":"NTNLSKMPNSUVULQPMPRKUQNPVSNQTSSQSODHLOTTELVHVSPQOVSOSTLRLOSNLLQNRSAURQKOTKRLUNQQLVTTOUPVRKUTKPLVPANLR","isNfc":false,"nfd":"8b6aa2eb9d495561","nfc":"c29922adb80b7354","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"721c5e3941d991c9","nfc":"721c5e3941d991c9","nfcEqual":true},{"in":"RSKOKOJSROKPVLOPLNKQPKNMMLPMRPPQQTQNUSSVQLKEKQPQVNKOMOMLVNTTTQVMKVNVQUOVLMRTTNUQRSTUHQTUOMTMQPMOUKNMUKO","isNfc":false,"nfd":"cad29bf625d5b9ba","nfc":"bc467df350e826ff","nfcEqual":false},{"in":"CNNNMMMMMMMMMMMMMMMMMMMMMMMMMMINMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"65dd0d031fb57d10","nfc":"4ca287e5211cef77","nfcEqual":true},{"in":"HQNTPSVTNUPQUTQNOKDKNTQNUVQRKKSMQQDRLVRSROKLOSTTTUPLSTLKSUSSQMU","isNfc":false,"nfd":"a5163e60883172ec","nfc":"56f2801500af21a8","nfcEqual":false},{"in":"DNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"d4f46d8645ff8ad7","nfc":"58a49a680b2db335","nfcEqual":false},{"in":"RTVVDVVPOOQOLSPRVRNOUMVPNOCULRQNMMQJUFPQLNVMSMRUMOMLRQNVKKUROOURQQPPPQLONSUNNUPVLQMSNTT","isNfc":false,"nfd":"3248071c2885c134","nfc":"d4bf3b46c4a7645d","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"e9767215b87b83bb","nfc":"dfcd79c0b0763955","nfcEqual":true},{"in":"MOTOMPNVROOVSKQPLVQMSLONVPPNVLPTKNSOOQMMHIUOBRNBQOOPVPQSNLNRRINSKOKTVROQTBUVOMRSNOOPOSMUPUKMR","isNfc":false,"nfd":"541089be5c4fde5d","nfc":"0100bb68e305f5ff","nfcEqual":false},{"in":"BMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"53c4a01b33e547b8","nfc":"53c4a01b33e547b8","nfcEqual":false},{"in":"EMVRUKUKRSOSNCMPMVPUPTSRUNLRUTOVKSOMKSRSMQTNOVPNUNPLPKLTLOR","isNfc":false,"nfd":"d664928716e11126","nfc":"62a4db88b74bed2f","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"dfe0f1f7dff99638","nfc":"830ca4156829b226","nfcEqual":true},{"in":"TLOOPVSMKCSTRTMLUOLQUPROMSVKKNMVKOAOLKTMMSPOPMQRNLSJKTLUQUPMMNVPUAOUO","isNfc":false,"nfd":"b3ee939191766aa5","nfc":"99363ff62340ce16","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"08f0cfeeb760a560","nfc":"08f0cfeeb760a560","nfcEqual":false},{"in":"MQQMVPRONBUPSONLRPKQUPUKRPUKSMLUQNVVSIHFPTKUMJQQMUTLOVLTLNNSTNVVMLVLRRL","isNfc":false,"nfd":"8497675e2f635f4b","nfc":"e7ab8500f2ab489f","nfcEqual":false},{"in":"DNMMMMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"6cb3108ef54e2f18","nfc":"ceeef02a90d56400","nfcEqual":true},{"in":"MQLRVQKNUPMELVROTMSQMUOPLPQPSUPVNL","isNfc":false,"nfd":"8a10cf43ffdba0ce","nfc":"a322bb7e60390566","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMINMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e09bd752a61b9fa3","nfc":"bdbddaa1b2f41f8f","nfcEqual":false},{"in":"MQOQNSTOTVUQTOUHNSRONKQLLPTQMMVNTSPOLSLSQMNTQQNPLTNVRMVOTDIMUVSQKPPPVOLTMSNSKLKORSKOOLKSLSMVQUNVTURTRKRMK","isNfc":false,"nfd":"3eadb893b1f4e82e","nfc":"8f879e948d98f2a2","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"cb62111d80b8fd54","nfc":"cb62111d80b8fd54","nfcEqual":false},{"in":"LQOTQQKTMVMPQMSKSTSORKKMVISQMPMH","isNfc":false,"nfd":"991824e614a8d92d","nfc":"62623807db62c10d","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"919ca6df39003143","nfc":"99fdae28714c2a42","nfcEqual":false},{"in":"BSULTMLQUVSUUPVRLTMVSLPOOMSLTVLKSKOQUOOPUNNSOVQTLUVMONSMMNLTQURTUOOCMNMMPSLSNQRLULMMTLVLTSSSKNSJKVSKQQ","isNfc":false,"nfd":"a969bea257018218","nfc":"9157fe2fec53658e","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNNNMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"19aa0b08d879f58e","nfc":"a3c71295820096d8","nfcEqual":false},{"in":"HCLRRVOOUOPKKLNPMQQTOTNBNNSUTTTOMOHRT","isNfc":false,"nfd":"2f72c2269fa3d830","nfc":"2f72c2269fa3d830","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"e0eefd5836eb8897","nfc":"c0de51ba099dbaba","nfcEqual":true},{"in":"MRUUQLNVVKMVVTTMRUQRSQVTHBMMVDQOVOMVMKVKMNVNVOQQPRULSBVKLSORJEPQNURKIVKONMFTTQNQNPPVRKNQBPORP","isNfc":false,"nfd":"f4df25cb71a5228c","nfc":"055b6d1c79dea67f","nfcEqual":false},{"in":"BNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"35473219127f8849","nfc":"35473219127f8849","nfcEqual":true},{"in":"ITUKRORTNLKQLSULUSPRKNSROTQKPLNPLRURVT","isNfc":false,"nfd":"9d924f85983982f2","nfc":"7c634ee180dc4f2f","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMANMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"30874a7dc224d35f","nfc":"3d27991b47bda03c","nfcEqual":true},{"in":"LTVNLMQQROKEPQKKVKMRRVNTNOMVMKKPRRK","isNfc":false,"nfd":"04a537692640828c","nfc":"0a9462477f684dbe","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"f77a0b2965042899","nfc":"20674ea7c36ead91","nfcEqual":false},{"in":"JEQTANRLTTERLORONLOFMOQLKMNGSQQULVLUQPNHRAMVBTKNVRQSLVLKL","isNfc":false,"nfd":"cbee85df64c1509e","nfc":"36cc97f019903ff2","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMDNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"100b259a5a2a12f3","nfc":"5544a2514992ddcf","nfcEqual":false},{"in":"ALPPTUSTVTOQPNPTSQQSRTVPELAKRMONKROPSPLNKSNMFSOSLLRFLKLLRQ","isNfc":false,"nfd":"7a43bf018f0f1304","nfc":"d7a69b9dc4f43b9d","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"eeb77475324e432f","nfc":"c6f686373b06f8e8","nfcEqual":false},{"in":"NENRLOUVPSSNNQTKMTQLMLKVVQQLTNMVVQKPSQLMUPQQKQNOQTNTKMPSKOQLQMOOKSVK","isNfc":false,"nfd":"5216c48ec8e2e7b1","nfc":"435ada5916c7a458","nfcEqual":false},{"in":"GNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"694e392c6df50cca","nfc":"694e392c6df50cca","nfcEqual":false},{"in":"UTOVTMUUKLTPGUOQVTSLQSVKQTUMLRNRMRTRTJQNFKUNSUTQQLRQVBCPTSVPOOLNCUKMM","isNfc":false,"nfd":"7fbd2d676e958f4f","nfc":"75c71712f23c27e0","nfcEqual":false},{"in":"ENNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a9b8de193d622435","nfc":"c4aff1fe9bda68a5","nfcEqual":false},{"in":"SVPLNUURMQVMVKUSUBTULNNRMKVULPMSVQVUSSMSKTRTOSTSUMQQOPPV","isNfc":false,"nfd":"8c7a07b6d62b0c3d","nfc":"8c7a07b6d62b0c3d","nfcEqual":false},{"in":"JNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"7071ae63e8556640","nfc":"575c80a07f5ad919","nfcEqual":false},{"in":"JJOQBTMQNEPOUNUKTTMSTPQNTLPNLRVQOPLUQMUNTVMSTLKSKMNMMMMRPM","isNfc":false,"nfd":"533b106be247ca1f","nfc":"f7690d69ce191ab9","nfcEqual":false},{"in":"INMMMMMMMMMMMMMMMMMMMMMMMMMMMMBMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"84080f543ab54f73","nfc":"c925f4d75c223704","nfcEqual":false},{"in":"SGOTRPMRNQLQPPOQTROVRKSORSTKNRVLQTSMOUUSTUVLKLVQNVUFKNLQVK","isNfc":false,"nfd":"8d2decf4647b2470","nfc":"8d2decf4647b2470","nfcEqual":false},{"in":"JNNNMMMMMMMMMMMMMMMMMMMMMMMMBNNMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"2e2f83fa069ee5ed","nfc":"29ee8bdaf51a7b3f","nfcEqual":true},{"in":"IBTKQKATVMKVKPQRLQNORPNTQNSNQTNOUSVKRQRNPORKLOHVLPUQUVVUVOPTNLVLOQ","isNfc":false,"nfd":"4522e5f65836a0fa","nfc":"075a287d80cb23bd","nfcEqual":false},{"in":"AMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"9f36a9f5a8581921","nfc":"9f36a9f5a8581921","nfcEqual":false},{"in":"BTMVRUNMSVLRKNQLPPOSLKTHOMTRRMQUL","isNfc":false,"nfd":"8c6750a813e10cbe","nfc":"8c6750a813e10cbe","nfcEqual":false},{"in":"GNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"2bcd7aea4d6e3894","nfc":"2bcd7aea4d6e3894","nfcEqual":false},{"in":"ALNLLTROUPVPOPMNNKSPNSQUMRUSO","isNfc":false,"nfd":"81a02c7bd6073e6e","nfc":"5dfb497b1c62ac83","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMANMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"141b5f9a19308d16","nfc":"141b5f9a19308d16","nfcEqual":false},{"in":"KJKMRRUQULRTVUKMVROUKNUVURKKMVKQUVC","isNfc":false,"nfd":"e45d89cd8234bbd9","nfc":"1a2ef245a0365b61","nfcEqual":false},{"in":"ANNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"6d5b67367d5c08e5","nfc":"6d5b67367d5c08e5","nfcEqual":false},{"in":"SKKQSULSKSVLRHNQVVLNANFPKOUNSTVT","isNfc":false,"nfd":"3d8e97acf50195a0","nfc":"3d8e97acf50195a0","nfcEqual":false},{"in":"GMMMMMMMMMMMMMMMMMMMMMMMMMMMMCMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"1213a53239df25d2","nfc":"1213a53239df25d2","nfcEqual":false},{"in":"MPHPKKRPRUSUVIQPLUSRNLKTTRMURKPNOVSQINTVPQVSQLMVVHTSKLTAUMLSV","isNfc":false,"nfd":"cfb950f28c67560e","nfc":"a476666478ebef9d","nfcEqual":false},{"in":"HNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMJNNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0fee7712885bfe73","nfc":"897b3f922bb7137c","nfcEqual":false},{"in":"QLNTRKMLQLSRQKPORSPORPUNPVUNOVOUURTOMPVOQVVTNRSNCQNRMLONELLQQUSLRRVSFOMQUKOKUSMSQOOLVSPRORRVNTKNNVQTMQPM","isNfc":false,"nfd":"47604036bb513a0a","nfc":"0e3942703fa53b89","nfcEqual":false},{"in":"ANMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"72d3be4b3b51a846","nfc":"72d3be4b3b51a846","nfcEqual":true},{"in":"KNOUMSRTOQKSSTRPKVOLUKNLQKNMPUU","isNfc":false,"nfd":"37073d71d7a1487d","nfc":"37073d71d7a1487d","nfcEqual":false},{"in":"INNNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"17e96a8378cff55a","nfc":"bb7b282c5968f076","nfcEqual":false},{"in":"SLKKBVVRRNRNRPLPSTMQVPVTSRKLQQTLLLURUMRPKUVURSVPKQVQCMRORPQQLLMHELNTRRMUONLNPQKSNRQKMNTORRROKU","isNfc":false,"nfd":"fc56c998070ab24c","nfc":"2c701314aa868cf0","nfcEqual":false},{"in":"IMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"18fc7a509ae11575","nfc":"16a60865cc0c918e","nfcEqual":false},{"in":"CNRRQVULLNPNRLVQROKUTNNSRMPTMSPPOLRNVVMPQVRSOUTUKOUMPSLNPLNLMPRCM","isNfc":false,"nfd":"a991ad8e115b2eea","nfc":"a991ad8e115b2eea","nfcEqual":false},{"in":"CMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"58b193c04a3d988f","nfc":"58b193c04a3d988f","nfcEqual":true},{"in":"RVLUKTRMVOPSSKNPRLLNLFRLLKKSFQSMKVOTTLQSIVMSRQTPSTSQTQQVVSS","isNfc":false,"nfd":"38f0077532b19f84","nfc":"8961c43c859a6ad8","nfcEqual":false},{"in":"JNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"7071ae63e8556640","nfc":"575c80a07f5ad919","nfcEqual":false},{"in":"RUPTLOKTMPSTPPLSMTQNVNPLNRRLMKUNUKQTP","isNfc":false,"nfd":"196d45b07b1beb5e","nfc":"196d45b07b1beb5e","nfcEqual":false},{"in":"CNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMHNMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"e78842b494373580","nfc":"e78842b494373580","nfcEqual":false},{"in":"VHMPRUJKMQNURNRPKSUOSLKOOQSKRPLNKOVT","isNfc":false,"nfd":"4a65e7ccfd6b3ca5","nfc":"217af268514d11c9","nfcEqual":false},{"in":"JNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"468ba6ab7f3347d5","nfc":"ad9b935b875f2cee","nfcEqual":false},{"in":"OLKPKNQUOOUHVVLLLNTRMNPNNUNPQTMURKKN","isNfc":false,"nfd":"211c902ec54322d6","nfc":"211c902ec54322d6","nfcEqual":false},{"in":"ENMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"9da1f265cc558b21","nfc":"48386e7704de24ea","nfcEqual":false},{"in":"SNAKIPVPRPVMTQKMNUCNKPOQNOGTLUB","isNfc":false,"nfd":"c6100d8ea893cb98","nfc":"9a68708303e5d385","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"a55998b439e236ca","nfc":"a55998b439e236ca","nfcEqual":false},{"in":"DVMOQSOSMTVMKMLTENOTOOLLOORQPSVTMKNORUVLVVPSTPVSMBTUPMQUTSNKRILQKVKRMRPNORLOSVPRMVPVUPLBLTKMSETFLMKSPK","isNfc":false,"nfd":"d80a67a05e016147","nfc":"eaebdc04bf449f92","nfcEqual":false},{"in":"EMMMMMMMMMMMMMMMMMMMMMMMMMMMMMANNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"bfc746709d043152","nfc":"f14aaf9ed73acc10","nfcEqual":false},{"in":"KMMTTKPRUSKTNLRRMMNMKQMLNOPRNKDTNLRTQKKVSPNVSMRSNFNVSMHMSPOTKKTPQKQRQMUSKUVRONULVVTLLRNRSTMVLTLOMVQ","isNfc":false,"nfd":"c50a59e92a31ceba","nfc":"d786402070a72ac4","nfcEqual":false},{"in":"GMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMBNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"3e6ee337d5424fd9","nfc":"3e6ee337d5424fd9","nfcEqual":false},{"in":"MVUPNNRPPOVSPSSMTOTPMPQLTSKSUNLLTKONRQRLUUSLLURTNMLSVMVU","isNfc":false,"nfd":"49afea1f34990d49","nfc":"49afea1f34990d49","nfcEqual":false},{"in":"FMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"27f8726ef503506f","nfc":"27f8726ef503506f","nfcEqual":true},{"in":"AQMKUSSQRRSKVLLQLNPVKPRURMQVVPVNTSQTSNUUQLURLPURSLUKOPNVLKLMSQTTOTNKSSNSQTPLQPMOLNSVTNSVMMSRT","isNfc":false,"nfd":"2b6456ecf2486616","nfc":"2b6456ecf2486616","nfcEqual":false},{"in":"DNNNMMMMMMMMMMMMMMMMMMMMMMMMINMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"0457df8c49fe6b13","nfc":"5329a81c2951163c","nfcEqual":false},{"in":"EFPRSKNULRPOQQRKTKSTANRVNRSPMURNKOQFQUKRKUKTNLQSNSPPQUQTNLMOMDSRKLSROPTTKKSRPQLQROSLPBUP","isNfc":false,"nfd":"8b6672270791b5df","nfc":"84416708bef115c3","nfcEqual":false},{"in":"INNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"b3b935500ee0726d","nfc":"469c0e2337c40e9f","nfcEqual":false},{"in":"TLUUQUNQPSVQMUKTTCKVNPURSLOVORRRQVQVPRTQMMKNSRLTOKOMOUVKQLS","isNfc":false,"nfd":"ef2e1bf7efc28e72","nfc":"ef2e1bf7efc28e72","nfcEqual":false},{"in":"HNNNMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"cfe8093559e85bce","nfc":"cfe8093559e85bce","nfcEqual":true},{"in":"SPTKNNVSTLTKLUUTLONIQSLVUMKOKRSSDOO","isNfc":false,"nfd":"49c590fd3e5f5d8c","nfc":"04ede8e0717aab66","nfcEqual":false},{"in":"JMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCNNMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"43d71d0b4e22509d","nfc":"67ce99ff560d4919","nfcEqual":false},{"in":"VVVURUKSRPLKQPRNTMTOPGRMLPPVOUUKKLROQTPTPKOLIQOOTFPLQRQTVTTKKRM","isNfc":false,"nfd":"dd577a3b9e845c9a","nfc":"f587b10df3b7b4f1","nfcEqual":false},{"in":"HNMMMMMMMMMMMMMMMMMMMMMMMMMHMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":true,"nfd":"23b31947af9ae420","nfc":"23b31947af9ae420","nfcEqual":true},{"in":"TOORKTPSOSTTRUTSNKLKVROKOOPKORQPRKTPSVKUKOLMFULSRONNVKQLOOMSUOPRRUVTUMUPVLNOVOPQSTTSOKPTRQ","isNfc":false,"nfd":"93bfeed1b099cbf6","nfc":"93bfeed1b099cbf6","nfcEqual":false},{"in":"HNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM","isNfc":false,"nfd":"aa2a013404eb6c14","nfc":"aa2a013404eb6c14","nfcEqual":false}]} diff --git a/packages/skill-tree/scripts/unicode-15.0.0-go.json b/packages/skill-tree/scripts/unicode-15.0.0-go.json index 85bb1df..4a3d353 100644 --- a/packages/skill-tree/scripts/unicode-15.0.0-go.json +++ b/packages/skill-tree/scripts/unicode-15.0.0-go.json @@ -1 +1 @@ -{"unicodeVersion":"15.0.0","normVersion":"15.0.0","goVersion":"go1.25.3","assigned":[[0,887],[890,895],[900,906],[908,908],[910,929],[931,1327],[1329,1366],[1369,1418],[1421,1423],[1425,1479],[1488,1514],[1519,1524],[1536,1805],[1807,1866],[1869,1969],[1984,2042],[2045,2093],[2096,2110],[2112,2139],[2142,2142],[2144,2154],[2160,2190],[2192,2193],[2200,2435],[2437,2444],[2447,2448],[2451,2472],[2474,2480],[2482,2482],[2486,2489],[2492,2500],[2503,2504],[2507,2510],[2519,2519],[2524,2525],[2527,2531],[2534,2558],[2561,2563],[2565,2570],[2575,2576],[2579,2600],[2602,2608],[2610,2611],[2613,2614],[2616,2617],[2620,2620],[2622,2626],[2631,2632],[2635,2637],[2641,2641],[2649,2652],[2654,2654],[2662,2678],[2689,2691],[2693,2701],[2703,2705],[2707,2728],[2730,2736],[2738,2739],[2741,2745],[2748,2757],[2759,2761],[2763,2765],[2768,2768],[2784,2787],[2790,2801],[2809,2815],[2817,2819],[2821,2828],[2831,2832],[2835,2856],[2858,2864],[2866,2867],[2869,2873],[2876,2884],[2887,2888],[2891,2893],[2901,2903],[2908,2909],[2911,2915],[2918,2935],[2946,2947],[2949,2954],[2958,2960],[2962,2965],[2969,2970],[2972,2972],[2974,2975],[2979,2980],[2984,2986],[2990,3001],[3006,3010],[3014,3016],[3018,3021],[3024,3024],[3031,3031],[3046,3066],[3072,3084],[3086,3088],[3090,3112],[3114,3129],[3132,3140],[3142,3144],[3146,3149],[3157,3158],[3160,3162],[3165,3165],[3168,3171],[3174,3183],[3191,3212],[3214,3216],[3218,3240],[3242,3251],[3253,3257],[3260,3268],[3270,3272],[3274,3277],[3285,3286],[3293,3294],[3296,3299],[3302,3311],[3313,3315],[3328,3340],[3342,3344],[3346,3396],[3398,3400],[3402,3407],[3412,3427],[3430,3455],[3457,3459],[3461,3478],[3482,3505],[3507,3515],[3517,3517],[3520,3526],[3530,3530],[3535,3540],[3542,3542],[3544,3551],[3558,3567],[3570,3572],[3585,3642],[3647,3675],[3713,3714],[3716,3716],[3718,3722],[3724,3747],[3749,3749],[3751,3773],[3776,3780],[3782,3782],[3784,3790],[3792,3801],[3804,3807],[3840,3911],[3913,3948],[3953,3991],[3993,4028],[4030,4044],[4046,4058],[4096,4293],[4295,4295],[4301,4301],[4304,4680],[4682,4685],[4688,4694],[4696,4696],[4698,4701],[4704,4744],[4746,4749],[4752,4784],[4786,4789],[4792,4798],[4800,4800],[4802,4805],[4808,4822],[4824,4880],[4882,4885],[4888,4954],[4957,4988],[4992,5017],[5024,5109],[5112,5117],[5120,5788],[5792,5880],[5888,5909],[5919,5942],[5952,5971],[5984,5996],[5998,6000],[6002,6003],[6016,6109],[6112,6121],[6128,6137],[6144,6169],[6176,6264],[6272,6314],[6320,6389],[6400,6430],[6432,6443],[6448,6459],[6464,6464],[6468,6509],[6512,6516],[6528,6571],[6576,6601],[6608,6618],[6622,6683],[6686,6750],[6752,6780],[6783,6793],[6800,6809],[6816,6829],[6832,6862],[6912,6988],[6992,7038],[7040,7155],[7164,7223],[7227,7241],[7245,7304],[7312,7354],[7357,7367],[7376,7418],[7424,7957],[7960,7965],[7968,8005],[8008,8013],[8016,8023],[8025,8025],[8027,8027],[8029,8029],[8031,8061],[8064,8116],[8118,8132],[8134,8147],[8150,8155],[8157,8175],[8178,8180],[8182,8190],[8192,8292],[8294,8305],[8308,8334],[8336,8348],[8352,8384],[8400,8432],[8448,8587],[8592,9254],[9280,9290],[9312,11123],[11126,11157],[11159,11507],[11513,11557],[11559,11559],[11565,11565],[11568,11623],[11631,11632],[11647,11670],[11680,11686],[11688,11694],[11696,11702],[11704,11710],[11712,11718],[11720,11726],[11728,11734],[11736,11742],[11744,11869],[11904,11929],[11931,12019],[12032,12245],[12272,12283],[12288,12351],[12353,12438],[12441,12543],[12549,12591],[12593,12686],[12688,12771],[12784,12830],[12832,42124],[42128,42182],[42192,42539],[42560,42743],[42752,42954],[42960,42961],[42963,42963],[42965,42969],[42994,43052],[43056,43065],[43072,43127],[43136,43205],[43214,43225],[43232,43347],[43359,43388],[43392,43469],[43471,43481],[43486,43518],[43520,43574],[43584,43597],[43600,43609],[43612,43714],[43739,43766],[43777,43782],[43785,43790],[43793,43798],[43808,43814],[43816,43822],[43824,43883],[43888,44013],[44016,44025],[44032,55203],[55216,55238],[55243,55291],[55296,64109],[64112,64217],[64256,64262],[64275,64279],[64285,64310],[64312,64316],[64318,64318],[64320,64321],[64323,64324],[64326,64450],[64467,64911],[64914,64967],[64975,64975],[65008,65049],[65056,65106],[65108,65126],[65128,65131],[65136,65140],[65142,65276],[65279,65279],[65281,65470],[65474,65479],[65482,65487],[65490,65495],[65498,65500],[65504,65510],[65512,65518],[65529,65533],[65536,65547],[65549,65574],[65576,65594],[65596,65597],[65599,65613],[65616,65629],[65664,65786],[65792,65794],[65799,65843],[65847,65934],[65936,65948],[65952,65952],[66000,66045],[66176,66204],[66208,66256],[66272,66299],[66304,66339],[66349,66378],[66384,66426],[66432,66461],[66463,66499],[66504,66517],[66560,66717],[66720,66729],[66736,66771],[66776,66811],[66816,66855],[66864,66915],[66927,66938],[66940,66954],[66956,66962],[66964,66965],[66967,66977],[66979,66993],[66995,67001],[67003,67004],[67072,67382],[67392,67413],[67424,67431],[67456,67461],[67463,67504],[67506,67514],[67584,67589],[67592,67592],[67594,67637],[67639,67640],[67644,67644],[67647,67669],[67671,67742],[67751,67759],[67808,67826],[67828,67829],[67835,67867],[67871,67897],[67903,67903],[67968,68023],[68028,68047],[68050,68099],[68101,68102],[68108,68115],[68117,68119],[68121,68149],[68152,68154],[68159,68168],[68176,68184],[68192,68255],[68288,68326],[68331,68342],[68352,68405],[68409,68437],[68440,68466],[68472,68497],[68505,68508],[68521,68527],[68608,68680],[68736,68786],[68800,68850],[68858,68903],[68912,68921],[69216,69246],[69248,69289],[69291,69293],[69296,69297],[69373,69415],[69424,69465],[69488,69513],[69552,69579],[69600,69622],[69632,69709],[69714,69749],[69759,69826],[69837,69837],[69840,69864],[69872,69881],[69888,69940],[69942,69959],[69968,70006],[70016,70111],[70113,70132],[70144,70161],[70163,70209],[70272,70278],[70280,70280],[70282,70285],[70287,70301],[70303,70313],[70320,70378],[70384,70393],[70400,70403],[70405,70412],[70415,70416],[70419,70440],[70442,70448],[70450,70451],[70453,70457],[70459,70468],[70471,70472],[70475,70477],[70480,70480],[70487,70487],[70493,70499],[70502,70508],[70512,70516],[70656,70747],[70749,70753],[70784,70855],[70864,70873],[71040,71093],[71096,71133],[71168,71236],[71248,71257],[71264,71276],[71296,71353],[71360,71369],[71424,71450],[71453,71467],[71472,71494],[71680,71739],[71840,71922],[71935,71942],[71945,71945],[71948,71955],[71957,71958],[71960,71989],[71991,71992],[71995,72006],[72016,72025],[72096,72103],[72106,72151],[72154,72164],[72192,72263],[72272,72354],[72368,72440],[72448,72457],[72704,72712],[72714,72758],[72760,72773],[72784,72812],[72816,72847],[72850,72871],[72873,72886],[72960,72966],[72968,72969],[72971,73014],[73018,73018],[73020,73021],[73023,73031],[73040,73049],[73056,73061],[73063,73064],[73066,73102],[73104,73105],[73107,73112],[73120,73129],[73440,73464],[73472,73488],[73490,73530],[73534,73561],[73648,73648],[73664,73713],[73727,74649],[74752,74862],[74864,74868],[74880,75075],[77712,77810],[77824,78933],[82944,83526],[92160,92728],[92736,92766],[92768,92777],[92782,92862],[92864,92873],[92880,92909],[92912,92917],[92928,92997],[93008,93017],[93019,93025],[93027,93047],[93053,93071],[93760,93850],[93952,94026],[94031,94087],[94095,94111],[94176,94180],[94192,94193],[94208,100343],[100352,101589],[101632,101640],[110576,110579],[110581,110587],[110589,110590],[110592,110882],[110898,110898],[110928,110930],[110933,110933],[110948,110951],[110960,111355],[113664,113770],[113776,113788],[113792,113800],[113808,113817],[113820,113827],[118528,118573],[118576,118598],[118608,118723],[118784,119029],[119040,119078],[119081,119274],[119296,119365],[119488,119507],[119520,119539],[119552,119638],[119648,119672],[119808,119892],[119894,119964],[119966,119967],[119970,119970],[119973,119974],[119977,119980],[119982,119993],[119995,119995],[119997,120003],[120005,120069],[120071,120074],[120077,120084],[120086,120092],[120094,120121],[120123,120126],[120128,120132],[120134,120134],[120138,120144],[120146,120485],[120488,120779],[120782,121483],[121499,121503],[121505,121519],[122624,122654],[122661,122666],[122880,122886],[122888,122904],[122907,122913],[122915,122916],[122918,122922],[122928,122989],[123023,123023],[123136,123180],[123184,123197],[123200,123209],[123214,123215],[123536,123566],[123584,123641],[123647,123647],[124112,124153],[124896,124902],[124904,124907],[124909,124910],[124912,124926],[124928,125124],[125127,125142],[125184,125259],[125264,125273],[125278,125279],[126065,126132],[126209,126269],[126464,126467],[126469,126495],[126497,126498],[126500,126500],[126503,126503],[126505,126514],[126516,126519],[126521,126521],[126523,126523],[126530,126530],[126535,126535],[126537,126537],[126539,126539],[126541,126543],[126545,126546],[126548,126548],[126551,126551],[126553,126553],[126555,126555],[126557,126557],[126559,126559],[126561,126562],[126564,126564],[126567,126570],[126572,126578],[126580,126583],[126585,126588],[126590,126590],[126592,126601],[126603,126619],[126625,126627],[126629,126633],[126635,126651],[126704,126705],[126976,127019],[127024,127123],[127136,127150],[127153,127167],[127169,127183],[127185,127221],[127232,127405],[127462,127490],[127504,127547],[127552,127560],[127568,127569],[127584,127589],[127744,128727],[128732,128748],[128752,128764],[128768,128886],[128891,128985],[128992,129003],[129008,129008],[129024,129035],[129040,129095],[129104,129113],[129120,129159],[129168,129197],[129200,129201],[129280,129619],[129632,129645],[129648,129660],[129664,129672],[129680,129725],[129727,129733],[129742,129755],[129760,129768],[129776,129784],[129792,129938],[129940,129994],[130032,130041],[131072,173791],[173824,177977],[177984,178205],[178208,183969],[183984,191456],[194560,195101],[196608,201546],[201552,205743],[917505,917505],[917536,917631],[917760,917999],[983040,1048573],[1048576,1114109]],"format":[[173,173],[1536,1541],[1564,1564],[1757,1757],[1807,1807],[2192,2193],[2274,2274],[6158,6158],[8203,8207],[8234,8238],[8288,8292],[8294,8303],[65279,65279],[65529,65531],[69821,69821],[69837,69837],[78896,78911],[113824,113827],[119155,119162],[917505,917505],[917536,917631]],"combiningClass":[[768,230],[769,230],[770,230],[771,230],[772,230],[773,230],[774,230],[775,230],[776,230],[777,230],[778,230],[779,230],[780,230],[781,230],[782,230],[783,230],[784,230],[785,230],[786,230],[787,230],[788,230],[789,232],[790,220],[791,220],[792,220],[793,220],[794,232],[795,216],[796,220],[797,220],[798,220],[799,220],[800,220],[801,202],[802,202],[803,220],[804,220],[805,220],[806,220],[807,202],[808,202],[809,220],[810,220],[811,220],[812,220],[813,220],[814,220],[815,220],[816,220],[817,220],[818,220],[819,220],[820,1],[821,1],[822,1],[823,1],[824,1],[825,220],[826,220],[827,220],[828,220],[829,230],[830,230],[831,230],[832,230],[833,230],[834,230],[835,230],[836,230],[837,240],[838,230],[839,220],[840,220],[841,220],[842,230],[843,230],[844,230],[845,220],[846,220],[848,230],[849,230],[850,230],[851,220],[852,220],[853,220],[854,220],[855,230],[856,232],[857,220],[858,220],[859,230],[860,233],[861,234],[862,234],[863,233],[864,234],[865,234],[866,233],[867,230],[868,230],[869,230],[870,230],[871,230],[872,230],[873,230],[874,230],[875,230],[876,230],[877,230],[878,230],[879,230],[1155,230],[1156,230],[1157,230],[1158,230],[1159,230],[1425,220],[1426,230],[1427,230],[1428,230],[1429,230],[1430,220],[1431,230],[1432,230],[1433,230],[1434,222],[1435,220],[1436,230],[1437,230],[1438,230],[1439,230],[1440,230],[1441,230],[1442,220],[1443,220],[1444,220],[1445,220],[1446,220],[1447,220],[1448,230],[1449,230],[1450,220],[1451,230],[1452,230],[1453,222],[1454,228],[1455,230],[1456,10],[1457,11],[1458,12],[1459,13],[1460,14],[1461,15],[1462,16],[1463,17],[1464,18],[1465,19],[1466,19],[1467,20],[1468,21],[1469,22],[1471,23],[1473,24],[1474,25],[1476,230],[1477,220],[1479,18],[1552,230],[1553,230],[1554,230],[1555,230],[1556,230],[1557,230],[1558,230],[1559,230],[1560,30],[1561,31],[1562,32],[1611,27],[1612,28],[1613,29],[1614,30],[1615,31],[1616,32],[1617,33],[1618,34],[1619,230],[1620,230],[1621,220],[1622,220],[1623,230],[1624,230],[1625,230],[1626,230],[1627,230],[1628,220],[1629,230],[1630,230],[1631,220],[1648,35],[1750,230],[1751,230],[1752,230],[1753,230],[1754,230],[1755,230],[1756,230],[1759,230],[1760,230],[1761,230],[1762,230],[1763,220],[1764,230],[1767,230],[1768,230],[1770,220],[1771,230],[1772,230],[1773,220],[1809,36],[1840,230],[1841,220],[1842,230],[1843,230],[1844,220],[1845,230],[1846,230],[1847,220],[1848,220],[1849,220],[1850,230],[1851,220],[1852,220],[1853,230],[1854,220],[1855,230],[1856,230],[1857,230],[1858,220],[1859,230],[1860,220],[1861,230],[1862,220],[1863,230],[1864,220],[1865,230],[1866,230],[2027,230],[2028,230],[2029,230],[2030,230],[2031,230],[2032,230],[2033,230],[2034,220],[2035,230],[2045,220],[2070,230],[2071,230],[2072,230],[2073,230],[2075,230],[2076,230],[2077,230],[2078,230],[2079,230],[2080,230],[2081,230],[2082,230],[2083,230],[2085,230],[2086,230],[2087,230],[2089,230],[2090,230],[2091,230],[2092,230],[2093,230],[2137,220],[2138,220],[2139,220],[2200,230],[2201,220],[2202,220],[2203,220],[2204,230],[2205,230],[2206,230],[2207,230],[2250,230],[2251,230],[2252,230],[2253,230],[2254,230],[2255,220],[2256,220],[2257,220],[2258,220],[2259,220],[2260,230],[2261,230],[2262,230],[2263,230],[2264,230],[2265,230],[2266,230],[2267,230],[2268,230],[2269,230],[2270,230],[2271,230],[2272,230],[2273,230],[2275,220],[2276,230],[2277,230],[2278,220],[2279,230],[2280,230],[2281,220],[2282,230],[2283,230],[2284,230],[2285,220],[2286,220],[2287,220],[2288,27],[2289,28],[2290,29],[2291,230],[2292,230],[2293,230],[2294,220],[2295,230],[2296,230],[2297,220],[2298,220],[2299,230],[2300,230],[2301,230],[2302,230],[2303,230],[2364,7],[2381,9],[2385,230],[2386,220],[2387,230],[2388,230],[2492,7],[2509,9],[2558,230],[2620,7],[2637,9],[2748,7],[2765,9],[2876,7],[2893,9],[3021,9],[3132,7],[3149,9],[3157,84],[3158,91],[3260,7],[3277,9],[3387,9],[3388,9],[3405,9],[3530,9],[3640,103],[3641,103],[3642,9],[3656,107],[3657,107],[3658,107],[3659,107],[3768,118],[3769,118],[3770,9],[3784,122],[3785,122],[3786,122],[3787,122],[3864,220],[3865,220],[3893,220],[3895,220],[3897,216],[3953,129],[3954,130],[3956,132],[3962,130],[3963,130],[3964,130],[3965,130],[3968,130],[3970,230],[3971,230],[3972,9],[3974,230],[3975,230],[4038,220],[4151,7],[4153,9],[4154,9],[4237,220],[4957,230],[4958,230],[4959,230],[5908,9],[5909,9],[5940,9],[6098,9],[6109,230],[6313,228],[6457,222],[6458,230],[6459,220],[6679,230],[6680,220],[6752,9],[6773,230],[6774,230],[6775,230],[6776,230],[6777,230],[6778,230],[6779,230],[6780,230],[6783,220],[6832,230],[6833,230],[6834,230],[6835,230],[6836,230],[6837,220],[6838,220],[6839,220],[6840,220],[6841,220],[6842,220],[6843,230],[6844,230],[6845,220],[6847,220],[6848,220],[6849,230],[6850,230],[6851,220],[6852,220],[6853,230],[6854,230],[6855,230],[6856,230],[6857,230],[6858,220],[6859,230],[6860,230],[6861,230],[6862,230],[6964,7],[6980,9],[7019,230],[7020,220],[7021,230],[7022,230],[7023,230],[7024,230],[7025,230],[7026,230],[7027,230],[7082,9],[7083,9],[7142,7],[7154,9],[7155,9],[7223,7],[7376,230],[7377,230],[7378,230],[7380,1],[7381,220],[7382,220],[7383,220],[7384,220],[7385,220],[7386,230],[7387,230],[7388,220],[7389,220],[7390,220],[7391,220],[7392,230],[7394,1],[7395,1],[7396,1],[7397,1],[7398,1],[7399,1],[7400,1],[7405,220],[7412,230],[7416,230],[7417,230],[7616,230],[7617,230],[7618,220],[7619,230],[7620,230],[7621,230],[7622,230],[7623,230],[7624,230],[7625,230],[7626,220],[7627,230],[7628,230],[7629,234],[7630,214],[7631,220],[7632,202],[7633,230],[7634,230],[7635,230],[7636,230],[7637,230],[7638,230],[7639,230],[7640,230],[7641,230],[7642,230],[7643,230],[7644,230],[7645,230],[7646,230],[7647,230],[7648,230],[7649,230],[7650,230],[7651,230],[7652,230],[7653,230],[7654,230],[7655,230],[7656,230],[7657,230],[7658,230],[7659,230],[7660,230],[7661,230],[7662,230],[7663,230],[7664,230],[7665,230],[7666,230],[7667,230],[7668,230],[7669,230],[7670,232],[7671,228],[7672,228],[7673,220],[7674,218],[7675,230],[7676,233],[7677,220],[7678,230],[7679,220],[8400,230],[8401,230],[8402,1],[8403,1],[8404,230],[8405,230],[8406,230],[8407,230],[8408,1],[8409,1],[8410,1],[8411,230],[8412,230],[8417,230],[8421,1],[8422,1],[8423,230],[8424,220],[8425,230],[8426,1],[8427,1],[8428,220],[8429,220],[8430,220],[8431,220],[8432,230],[11503,230],[11504,230],[11505,230],[11647,9],[11744,230],[11745,230],[11746,230],[11747,230],[11748,230],[11749,230],[11750,230],[11751,230],[11752,230],[11753,230],[11754,230],[11755,230],[11756,230],[11757,230],[11758,230],[11759,230],[11760,230],[11761,230],[11762,230],[11763,230],[11764,230],[11765,230],[11766,230],[11767,230],[11768,230],[11769,230],[11770,230],[11771,230],[11772,230],[11773,230],[11774,230],[11775,230],[12330,218],[12331,228],[12332,232],[12333,222],[12334,224],[12335,224],[12441,8],[12442,8],[42607,230],[42612,230],[42613,230],[42614,230],[42615,230],[42616,230],[42617,230],[42618,230],[42619,230],[42620,230],[42621,230],[42654,230],[42655,230],[42736,230],[42737,230],[43014,9],[43052,9],[43204,9],[43232,230],[43233,230],[43234,230],[43235,230],[43236,230],[43237,230],[43238,230],[43239,230],[43240,230],[43241,230],[43242,230],[43243,230],[43244,230],[43245,230],[43246,230],[43247,230],[43248,230],[43249,230],[43307,220],[43308,220],[43309,220],[43347,9],[43443,7],[43456,9],[43696,230],[43698,230],[43699,230],[43700,220],[43703,230],[43704,230],[43710,230],[43711,230],[43713,230],[43766,9],[44013,9],[64286,26],[65056,230],[65057,230],[65058,230],[65059,230],[65060,230],[65061,230],[65062,230],[65063,220],[65064,220],[65065,220],[65066,220],[65067,220],[65068,220],[65069,220],[65070,230],[65071,230],[66045,220],[66272,220],[66422,230],[66423,230],[66424,230],[66425,230],[66426,230],[68109,220],[68111,230],[68152,230],[68153,1],[68154,220],[68159,9],[68325,230],[68326,220],[68900,230],[68901,230],[68902,230],[68903,230],[69291,230],[69292,230],[69373,220],[69374,220],[69375,220],[69446,220],[69447,220],[69448,230],[69449,230],[69450,230],[69451,220],[69452,230],[69453,220],[69454,220],[69455,220],[69456,220],[69506,230],[69507,220],[69508,230],[69509,220],[69702,9],[69744,9],[69759,9],[69817,9],[69818,7],[69888,230],[69889,230],[69890,230],[69939,9],[69940,9],[70003,7],[70080,9],[70090,7],[70197,9],[70198,7],[70377,7],[70378,9],[70459,7],[70460,7],[70477,9],[70502,230],[70503,230],[70504,230],[70505,230],[70506,230],[70507,230],[70508,230],[70512,230],[70513,230],[70514,230],[70515,230],[70516,230],[70722,9],[70726,7],[70750,230],[70850,9],[70851,7],[71103,9],[71104,7],[71231,9],[71350,9],[71351,7],[71467,9],[71737,9],[71738,7],[71997,9],[71998,9],[72003,7],[72160,9],[72244,9],[72263,9],[72345,9],[72767,9],[73026,7],[73028,9],[73029,9],[73111,9],[73537,9],[73538,9],[92912,1],[92913,1],[92914,1],[92915,1],[92916,1],[92976,230],[92977,230],[92978,230],[92979,230],[92980,230],[92981,230],[92982,230],[94192,6],[94193,6],[113822,1],[119141,216],[119142,216],[119143,1],[119144,1],[119145,1],[119149,226],[119150,216],[119151,216],[119152,216],[119153,216],[119154,216],[119163,220],[119164,220],[119165,220],[119166,220],[119167,220],[119168,220],[119169,220],[119170,220],[119173,230],[119174,230],[119175,230],[119176,230],[119177,230],[119178,220],[119179,220],[119210,230],[119211,230],[119212,230],[119213,230],[119362,230],[119363,230],[119364,230],[122880,230],[122881,230],[122882,230],[122883,230],[122884,230],[122885,230],[122886,230],[122888,230],[122889,230],[122890,230],[122891,230],[122892,230],[122893,230],[122894,230],[122895,230],[122896,230],[122897,230],[122898,230],[122899,230],[122900,230],[122901,230],[122902,230],[122903,230],[122904,230],[122907,230],[122908,230],[122909,230],[122910,230],[122911,230],[122912,230],[122913,230],[122915,230],[122916,230],[122918,230],[122919,230],[122920,230],[122921,230],[122922,230],[123023,230],[123184,230],[123185,230],[123186,230],[123187,230],[123188,230],[123189,230],[123190,230],[123566,230],[123628,230],[123629,230],[123630,230],[123631,230],[124140,232],[124141,232],[124142,220],[124143,230],[125136,220],[125137,220],[125138,220],[125139,220],[125140,220],[125141,220],[125142,220],[125252,230],[125253,230],[125254,230],[125255,230],[125256,230],[125257,230],[125258,7]],"decompositions":[[192,65,768],[193,65,769],[194,65,770],[195,65,771],[196,65,776],[197,65,778],[199,67,807],[200,69,768],[201,69,769],[202,69,770],[203,69,776],[204,73,768],[205,73,769],[206,73,770],[207,73,776],[209,78,771],[210,79,768],[211,79,769],[212,79,770],[213,79,771],[214,79,776],[217,85,768],[218,85,769],[219,85,770],[220,85,776],[221,89,769],[224,97,768],[225,97,769],[226,97,770],[227,97,771],[228,97,776],[229,97,778],[231,99,807],[232,101,768],[233,101,769],[234,101,770],[235,101,776],[236,105,768],[237,105,769],[238,105,770],[239,105,776],[241,110,771],[242,111,768],[243,111,769],[244,111,770],[245,111,771],[246,111,776],[249,117,768],[250,117,769],[251,117,770],[252,117,776],[253,121,769],[255,121,776],[256,65,772],[257,97,772],[258,65,774],[259,97,774],[260,65,808],[261,97,808],[262,67,769],[263,99,769],[264,67,770],[265,99,770],[266,67,775],[267,99,775],[268,67,780],[269,99,780],[270,68,780],[271,100,780],[274,69,772],[275,101,772],[276,69,774],[277,101,774],[278,69,775],[279,101,775],[280,69,808],[281,101,808],[282,69,780],[283,101,780],[284,71,770],[285,103,770],[286,71,774],[287,103,774],[288,71,775],[289,103,775],[290,71,807],[291,103,807],[292,72,770],[293,104,770],[296,73,771],[297,105,771],[298,73,772],[299,105,772],[300,73,774],[301,105,774],[302,73,808],[303,105,808],[304,73,775],[308,74,770],[309,106,770],[310,75,807],[311,107,807],[313,76,769],[314,108,769],[315,76,807],[316,108,807],[317,76,780],[318,108,780],[323,78,769],[324,110,769],[325,78,807],[326,110,807],[327,78,780],[328,110,780],[332,79,772],[333,111,772],[334,79,774],[335,111,774],[336,79,779],[337,111,779],[340,82,769],[341,114,769],[342,82,807],[343,114,807],[344,82,780],[345,114,780],[346,83,769],[347,115,769],[348,83,770],[349,115,770],[350,83,807],[351,115,807],[352,83,780],[353,115,780],[354,84,807],[355,116,807],[356,84,780],[357,116,780],[360,85,771],[361,117,771],[362,85,772],[363,117,772],[364,85,774],[365,117,774],[366,85,778],[367,117,778],[368,85,779],[369,117,779],[370,85,808],[371,117,808],[372,87,770],[373,119,770],[374,89,770],[375,121,770],[376,89,776],[377,90,769],[378,122,769],[379,90,775],[380,122,775],[381,90,780],[382,122,780],[416,79,795],[417,111,795],[431,85,795],[432,117,795],[461,65,780],[462,97,780],[463,73,780],[464,105,780],[465,79,780],[466,111,780],[467,85,780],[468,117,780],[469,220,772],[470,252,772],[471,220,769],[472,252,769],[473,220,780],[474,252,780],[475,220,768],[476,252,768],[478,196,772],[479,228,772],[480,550,772],[481,551,772],[482,198,772],[483,230,772],[486,71,780],[487,103,780],[488,75,780],[489,107,780],[490,79,808],[491,111,808],[492,490,772],[493,491,772],[494,439,780],[495,658,780],[496,106,780],[500,71,769],[501,103,769],[504,78,768],[505,110,768],[506,197,769],[507,229,769],[508,198,769],[509,230,769],[510,216,769],[511,248,769],[512,65,783],[513,97,783],[514,65,785],[515,97,785],[516,69,783],[517,101,783],[518,69,785],[519,101,785],[520,73,783],[521,105,783],[522,73,785],[523,105,785],[524,79,783],[525,111,783],[526,79,785],[527,111,785],[528,82,783],[529,114,783],[530,82,785],[531,114,785],[532,85,783],[533,117,783],[534,85,785],[535,117,785],[536,83,806],[537,115,806],[538,84,806],[539,116,806],[542,72,780],[543,104,780],[550,65,775],[551,97,775],[552,69,807],[553,101,807],[554,214,772],[555,246,772],[556,213,772],[557,245,772],[558,79,775],[559,111,775],[560,558,772],[561,559,772],[562,89,772],[563,121,772],[832,768],[833,769],[835,787],[836,776,769],[884,697],[894,59],[901,168,769],[902,913,769],[903,183],[904,917,769],[905,919,769],[906,921,769],[908,927,769],[910,933,769],[911,937,769],[912,970,769],[938,921,776],[939,933,776],[940,945,769],[941,949,769],[942,951,769],[943,953,769],[944,971,769],[970,953,776],[971,965,776],[972,959,769],[973,965,769],[974,969,769],[979,978,769],[980,978,776],[1024,1045,768],[1025,1045,776],[1027,1043,769],[1031,1030,776],[1036,1050,769],[1037,1048,768],[1038,1059,774],[1049,1048,774],[1081,1080,774],[1104,1077,768],[1105,1077,776],[1107,1075,769],[1111,1110,776],[1116,1082,769],[1117,1080,768],[1118,1091,774],[1142,1140,783],[1143,1141,783],[1217,1046,774],[1218,1078,774],[1232,1040,774],[1233,1072,774],[1234,1040,776],[1235,1072,776],[1238,1045,774],[1239,1077,774],[1242,1240,776],[1243,1241,776],[1244,1046,776],[1245,1078,776],[1246,1047,776],[1247,1079,776],[1250,1048,772],[1251,1080,772],[1252,1048,776],[1253,1080,776],[1254,1054,776],[1255,1086,776],[1258,1256,776],[1259,1257,776],[1260,1069,776],[1261,1101,776],[1262,1059,772],[1263,1091,772],[1264,1059,776],[1265,1091,776],[1266,1059,779],[1267,1091,779],[1268,1063,776],[1269,1095,776],[1272,1067,776],[1273,1099,776],[1570,1575,1619],[1571,1575,1620],[1572,1608,1620],[1573,1575,1621],[1574,1610,1620],[1728,1749,1620],[1730,1729,1620],[1747,1746,1620],[2345,2344,2364],[2353,2352,2364],[2356,2355,2364],[2392,2325,2364],[2393,2326,2364],[2394,2327,2364],[2395,2332,2364],[2396,2337,2364],[2397,2338,2364],[2398,2347,2364],[2399,2351,2364],[2507,2503,2494],[2508,2503,2519],[2524,2465,2492],[2525,2466,2492],[2527,2479,2492],[2611,2610,2620],[2614,2616,2620],[2649,2582,2620],[2650,2583,2620],[2651,2588,2620],[2654,2603,2620],[2888,2887,2902],[2891,2887,2878],[2892,2887,2903],[2908,2849,2876],[2909,2850,2876],[2964,2962,3031],[3018,3014,3006],[3019,3015,3006],[3020,3014,3031],[3144,3142,3158],[3264,3263,3285],[3271,3270,3285],[3272,3270,3286],[3274,3270,3266],[3275,3274,3285],[3402,3398,3390],[3403,3399,3390],[3404,3398,3415],[3546,3545,3530],[3548,3545,3535],[3549,3548,3530],[3550,3545,3551],[3907,3906,4023],[3917,3916,4023],[3922,3921,4023],[3927,3926,4023],[3932,3931,4023],[3945,3904,4021],[3955,3953,3954],[3957,3953,3956],[3958,4018,3968],[3960,4019,3968],[3969,3953,3968],[3987,3986,4023],[3997,3996,4023],[4002,4001,4023],[4007,4006,4023],[4012,4011,4023],[4025,3984,4021],[4134,4133,4142],[6918,6917,6965],[6920,6919,6965],[6922,6921,6965],[6924,6923,6965],[6926,6925,6965],[6930,6929,6965],[6971,6970,6965],[6973,6972,6965],[6976,6974,6965],[6977,6975,6965],[6979,6978,6965],[7680,65,805],[7681,97,805],[7682,66,775],[7683,98,775],[7684,66,803],[7685,98,803],[7686,66,817],[7687,98,817],[7688,199,769],[7689,231,769],[7690,68,775],[7691,100,775],[7692,68,803],[7693,100,803],[7694,68,817],[7695,100,817],[7696,68,807],[7697,100,807],[7698,68,813],[7699,100,813],[7700,274,768],[7701,275,768],[7702,274,769],[7703,275,769],[7704,69,813],[7705,101,813],[7706,69,816],[7707,101,816],[7708,552,774],[7709,553,774],[7710,70,775],[7711,102,775],[7712,71,772],[7713,103,772],[7714,72,775],[7715,104,775],[7716,72,803],[7717,104,803],[7718,72,776],[7719,104,776],[7720,72,807],[7721,104,807],[7722,72,814],[7723,104,814],[7724,73,816],[7725,105,816],[7726,207,769],[7727,239,769],[7728,75,769],[7729,107,769],[7730,75,803],[7731,107,803],[7732,75,817],[7733,107,817],[7734,76,803],[7735,108,803],[7736,7734,772],[7737,7735,772],[7738,76,817],[7739,108,817],[7740,76,813],[7741,108,813],[7742,77,769],[7743,109,769],[7744,77,775],[7745,109,775],[7746,77,803],[7747,109,803],[7748,78,775],[7749,110,775],[7750,78,803],[7751,110,803],[7752,78,817],[7753,110,817],[7754,78,813],[7755,110,813],[7756,213,769],[7757,245,769],[7758,213,776],[7759,245,776],[7760,332,768],[7761,333,768],[7762,332,769],[7763,333,769],[7764,80,769],[7765,112,769],[7766,80,775],[7767,112,775],[7768,82,775],[7769,114,775],[7770,82,803],[7771,114,803],[7772,7770,772],[7773,7771,772],[7774,82,817],[7775,114,817],[7776,83,775],[7777,115,775],[7778,83,803],[7779,115,803],[7780,346,775],[7781,347,775],[7782,352,775],[7783,353,775],[7784,7778,775],[7785,7779,775],[7786,84,775],[7787,116,775],[7788,84,803],[7789,116,803],[7790,84,817],[7791,116,817],[7792,84,813],[7793,116,813],[7794,85,804],[7795,117,804],[7796,85,816],[7797,117,816],[7798,85,813],[7799,117,813],[7800,360,769],[7801,361,769],[7802,362,776],[7803,363,776],[7804,86,771],[7805,118,771],[7806,86,803],[7807,118,803],[7808,87,768],[7809,119,768],[7810,87,769],[7811,119,769],[7812,87,776],[7813,119,776],[7814,87,775],[7815,119,775],[7816,87,803],[7817,119,803],[7818,88,775],[7819,120,775],[7820,88,776],[7821,120,776],[7822,89,775],[7823,121,775],[7824,90,770],[7825,122,770],[7826,90,803],[7827,122,803],[7828,90,817],[7829,122,817],[7830,104,817],[7831,116,776],[7832,119,778],[7833,121,778],[7835,383,775],[7840,65,803],[7841,97,803],[7842,65,777],[7843,97,777],[7844,194,769],[7845,226,769],[7846,194,768],[7847,226,768],[7848,194,777],[7849,226,777],[7850,194,771],[7851,226,771],[7852,7840,770],[7853,7841,770],[7854,258,769],[7855,259,769],[7856,258,768],[7857,259,768],[7858,258,777],[7859,259,777],[7860,258,771],[7861,259,771],[7862,7840,774],[7863,7841,774],[7864,69,803],[7865,101,803],[7866,69,777],[7867,101,777],[7868,69,771],[7869,101,771],[7870,202,769],[7871,234,769],[7872,202,768],[7873,234,768],[7874,202,777],[7875,234,777],[7876,202,771],[7877,234,771],[7878,7864,770],[7879,7865,770],[7880,73,777],[7881,105,777],[7882,73,803],[7883,105,803],[7884,79,803],[7885,111,803],[7886,79,777],[7887,111,777],[7888,212,769],[7889,244,769],[7890,212,768],[7891,244,768],[7892,212,777],[7893,244,777],[7894,212,771],[7895,244,771],[7896,7884,770],[7897,7885,770],[7898,416,769],[7899,417,769],[7900,416,768],[7901,417,768],[7902,416,777],[7903,417,777],[7904,416,771],[7905,417,771],[7906,416,803],[7907,417,803],[7908,85,803],[7909,117,803],[7910,85,777],[7911,117,777],[7912,431,769],[7913,432,769],[7914,431,768],[7915,432,768],[7916,431,777],[7917,432,777],[7918,431,771],[7919,432,771],[7920,431,803],[7921,432,803],[7922,89,768],[7923,121,768],[7924,89,803],[7925,121,803],[7926,89,777],[7927,121,777],[7928,89,771],[7929,121,771],[7936,945,787],[7937,945,788],[7938,7936,768],[7939,7937,768],[7940,7936,769],[7941,7937,769],[7942,7936,834],[7943,7937,834],[7944,913,787],[7945,913,788],[7946,7944,768],[7947,7945,768],[7948,7944,769],[7949,7945,769],[7950,7944,834],[7951,7945,834],[7952,949,787],[7953,949,788],[7954,7952,768],[7955,7953,768],[7956,7952,769],[7957,7953,769],[7960,917,787],[7961,917,788],[7962,7960,768],[7963,7961,768],[7964,7960,769],[7965,7961,769],[7968,951,787],[7969,951,788],[7970,7968,768],[7971,7969,768],[7972,7968,769],[7973,7969,769],[7974,7968,834],[7975,7969,834],[7976,919,787],[7977,919,788],[7978,7976,768],[7979,7977,768],[7980,7976,769],[7981,7977,769],[7982,7976,834],[7983,7977,834],[7984,953,787],[7985,953,788],[7986,7984,768],[7987,7985,768],[7988,7984,769],[7989,7985,769],[7990,7984,834],[7991,7985,834],[7992,921,787],[7993,921,788],[7994,7992,768],[7995,7993,768],[7996,7992,769],[7997,7993,769],[7998,7992,834],[7999,7993,834],[8000,959,787],[8001,959,788],[8002,8000,768],[8003,8001,768],[8004,8000,769],[8005,8001,769],[8008,927,787],[8009,927,788],[8010,8008,768],[8011,8009,768],[8012,8008,769],[8013,8009,769],[8016,965,787],[8017,965,788],[8018,8016,768],[8019,8017,768],[8020,8016,769],[8021,8017,769],[8022,8016,834],[8023,8017,834],[8025,933,788],[8027,8025,768],[8029,8025,769],[8031,8025,834],[8032,969,787],[8033,969,788],[8034,8032,768],[8035,8033,768],[8036,8032,769],[8037,8033,769],[8038,8032,834],[8039,8033,834],[8040,937,787],[8041,937,788],[8042,8040,768],[8043,8041,768],[8044,8040,769],[8045,8041,769],[8046,8040,834],[8047,8041,834],[8048,945,768],[8049,945,769],[8050,949,768],[8051,949,769],[8052,951,768],[8053,951,769],[8054,953,768],[8055,953,769],[8056,959,768],[8057,959,769],[8058,965,768],[8059,965,769],[8060,969,768],[8061,969,769],[8064,7936,837],[8065,7937,837],[8066,7938,837],[8067,7939,837],[8068,7940,837],[8069,7941,837],[8070,7942,837],[8071,7943,837],[8072,7944,837],[8073,7945,837],[8074,7946,837],[8075,7947,837],[8076,7948,837],[8077,7949,837],[8078,7950,837],[8079,7951,837],[8080,7968,837],[8081,7969,837],[8082,7970,837],[8083,7971,837],[8084,7972,837],[8085,7973,837],[8086,7974,837],[8087,7975,837],[8088,7976,837],[8089,7977,837],[8090,7978,837],[8091,7979,837],[8092,7980,837],[8093,7981,837],[8094,7982,837],[8095,7983,837],[8096,8032,837],[8097,8033,837],[8098,8034,837],[8099,8035,837],[8100,8036,837],[8101,8037,837],[8102,8038,837],[8103,8039,837],[8104,8040,837],[8105,8041,837],[8106,8042,837],[8107,8043,837],[8108,8044,837],[8109,8045,837],[8110,8046,837],[8111,8047,837],[8112,945,774],[8113,945,772],[8114,8048,837],[8115,945,837],[8116,940,837],[8118,945,834],[8119,8118,837],[8120,913,774],[8121,913,772],[8122,913,768],[8123,913,769],[8124,913,837],[8126,953],[8129,168,834],[8130,8052,837],[8131,951,837],[8132,942,837],[8134,951,834],[8135,8134,837],[8136,917,768],[8137,917,769],[8138,919,768],[8139,919,769],[8140,919,837],[8141,8127,768],[8142,8127,769],[8143,8127,834],[8144,953,774],[8145,953,772],[8146,970,768],[8147,953,776,769],[8150,953,834],[8151,970,834],[8152,921,774],[8153,921,772],[8154,921,768],[8155,921,769],[8157,8190,768],[8158,8190,769],[8159,8190,834],[8160,965,774],[8161,965,772],[8162,971,768],[8163,965,776,769],[8164,961,787],[8165,961,788],[8166,965,834],[8167,971,834],[8168,933,774],[8169,933,772],[8170,933,768],[8171,933,769],[8172,929,788],[8173,168,768],[8174,168,769],[8175,96],[8178,8060,837],[8179,969,837],[8180,974,837],[8182,969,834],[8183,8182,837],[8184,927,768],[8185,927,769],[8186,937,768],[8187,937,769],[8188,937,837],[8189,180],[8192,8194],[8193,8195],[8486,937],[8490,75],[8491,65,778],[8602,8592,824],[8603,8594,824],[8622,8596,824],[8653,8656,824],[8654,8660,824],[8655,8658,824],[8708,8707,824],[8713,8712,824],[8716,8715,824],[8740,8739,824],[8742,8741,824],[8769,8764,824],[8772,8771,824],[8775,8773,824],[8777,8776,824],[8800,61,824],[8802,8801,824],[8813,8781,824],[8814,60,824],[8815,62,824],[8816,8804,824],[8817,8805,824],[8820,8818,824],[8821,8819,824],[8824,8822,824],[8825,8823,824],[8832,8826,824],[8833,8827,824],[8836,8834,824],[8837,8835,824],[8840,8838,824],[8841,8839,824],[8876,8866,824],[8877,8872,824],[8878,8873,824],[8879,8875,824],[8928,8828,824],[8929,8829,824],[8930,8849,824],[8931,8850,824],[8938,8882,824],[8939,8883,824],[8940,8884,824],[8941,8885,824],[9001,12296],[9002,12297],[10972,10973,824],[12364,12363,12441],[12366,12365,12441],[12368,12367,12441],[12370,12369,12441],[12372,12371,12441],[12374,12373,12441],[12376,12375,12441],[12378,12377,12441],[12380,12379,12441],[12382,12381,12441],[12384,12383,12441],[12386,12385,12441],[12389,12388,12441],[12391,12390,12441],[12393,12392,12441],[12400,12399,12441],[12401,12399,12442],[12403,12402,12441],[12404,12402,12442],[12406,12405,12441],[12407,12405,12442],[12409,12408,12441],[12410,12408,12442],[12412,12411,12441],[12413,12411,12442],[12436,12358,12441],[12446,12445,12441],[12460,12459,12441],[12462,12461,12441],[12464,12463,12441],[12466,12465,12441],[12468,12467,12441],[12470,12469,12441],[12472,12471,12441],[12474,12473,12441],[12476,12475,12441],[12478,12477,12441],[12480,12479,12441],[12482,12481,12441],[12485,12484,12441],[12487,12486,12441],[12489,12488,12441],[12496,12495,12441],[12497,12495,12442],[12499,12498,12441],[12500,12498,12442],[12502,12501,12441],[12503,12501,12442],[12505,12504,12441],[12506,12504,12442],[12508,12507,12441],[12509,12507,12442],[12532,12454,12441],[12535,12527,12441],[12536,12528,12441],[12537,12529,12441],[12538,12530,12441],[12542,12541,12441],[63744,35912],[63745,26356],[63746,36554],[63747,36040],[63748,28369],[63749,20018],[63750,21477],[63751,40860],[63752,40860],[63753,22865],[63754,37329],[63755,21895],[63756,22856],[63757,25078],[63758,30313],[63759,32645],[63760,34367],[63761,34746],[63762,35064],[63763,37007],[63764,27138],[63765,27931],[63766,28889],[63767,29662],[63768,33853],[63769,37226],[63770,39409],[63771,20098],[63772,21365],[63773,27396],[63774,29211],[63775,34349],[63776,40478],[63777,23888],[63778,28651],[63779,34253],[63780,35172],[63781,25289],[63782,33240],[63783,34847],[63784,24266],[63785,26391],[63786,28010],[63787,29436],[63788,37070],[63789,20358],[63790,20919],[63791,21214],[63792,25796],[63793,27347],[63794,29200],[63795,30439],[63796,32769],[63797,34310],[63798,34396],[63799,36335],[63800,38706],[63801,39791],[63802,40442],[63803,30860],[63804,31103],[63805,32160],[63806,33737],[63807,37636],[63808,40575],[63809,35542],[63810,22751],[63811,24324],[63812,31840],[63813,32894],[63814,29282],[63815,30922],[63816,36034],[63817,38647],[63818,22744],[63819,23650],[63820,27155],[63821,28122],[63822,28431],[63823,32047],[63824,32311],[63825,38475],[63826,21202],[63827,32907],[63828,20956],[63829,20940],[63830,31260],[63831,32190],[63832,33777],[63833,38517],[63834,35712],[63835,25295],[63836,27138],[63837,35582],[63838,20025],[63839,23527],[63840,24594],[63841,29575],[63842,30064],[63843,21271],[63844,30971],[63845,20415],[63846,24489],[63847,19981],[63848,27852],[63849,25976],[63850,32034],[63851,21443],[63852,22622],[63853,30465],[63854,33865],[63855,35498],[63856,27578],[63857,36784],[63858,27784],[63859,25342],[63860,33509],[63861,25504],[63862,30053],[63863,20142],[63864,20841],[63865,20937],[63866,26753],[63867,31975],[63868,33391],[63869,35538],[63870,37327],[63871,21237],[63872,21570],[63873,22899],[63874,24300],[63875,26053],[63876,28670],[63877,31018],[63878,38317],[63879,39530],[63880,40599],[63881,40654],[63882,21147],[63883,26310],[63884,27511],[63885,36706],[63886,24180],[63887,24976],[63888,25088],[63889,25754],[63890,28451],[63891,29001],[63892,29833],[63893,31178],[63894,32244],[63895,32879],[63896,36646],[63897,34030],[63898,36899],[63899,37706],[63900,21015],[63901,21155],[63902,21693],[63903,28872],[63904,35010],[63905,35498],[63906,24265],[63907,24565],[63908,25467],[63909,27566],[63910,31806],[63911,29557],[63912,20196],[63913,22265],[63914,23527],[63915,23994],[63916,24604],[63917,29618],[63918,29801],[63919,32666],[63920,32838],[63921,37428],[63922,38646],[63923,38728],[63924,38936],[63925,20363],[63926,31150],[63927,37300],[63928,38584],[63929,24801],[63930,20102],[63931,20698],[63932,23534],[63933,23615],[63934,26009],[63935,27138],[63936,29134],[63937,30274],[63938,34044],[63939,36988],[63940,40845],[63941,26248],[63942,38446],[63943,21129],[63944,26491],[63945,26611],[63946,27969],[63947,28316],[63948,29705],[63949,30041],[63950,30827],[63951,32016],[63952,39006],[63953,20845],[63954,25134],[63955,38520],[63956,20523],[63957,23833],[63958,28138],[63959,36650],[63960,24459],[63961,24900],[63962,26647],[63963,29575],[63964,38534],[63965,21033],[63966,21519],[63967,23653],[63968,26131],[63969,26446],[63970,26792],[63971,27877],[63972,29702],[63973,30178],[63974,32633],[63975,35023],[63976,35041],[63977,37324],[63978,38626],[63979,21311],[63980,28346],[63981,21533],[63982,29136],[63983,29848],[63984,34298],[63985,38563],[63986,40023],[63987,40607],[63988,26519],[63989,28107],[63990,33256],[63991,31435],[63992,31520],[63993,31890],[63994,29376],[63995,28825],[63996,35672],[63997,20160],[63998,33590],[63999,21050],[64000,20999],[64001,24230],[64002,25299],[64003,31958],[64004,23429],[64005,27934],[64006,26292],[64007,36667],[64008,34892],[64009,38477],[64010,35211],[64011,24275],[64012,20800],[64013,21952],[64016,22618],[64018,26228],[64021,20958],[64022,29482],[64023,30410],[64024,31036],[64025,31070],[64026,31077],[64027,31119],[64028,38742],[64029,31934],[64030,32701],[64032,34322],[64034,35576],[64037,36920],[64038,37117],[64042,39151],[64043,39164],[64044,39208],[64045,40372],[64046,37086],[64047,38583],[64048,20398],[64049,20711],[64050,20813],[64051,21193],[64052,21220],[64053,21329],[64054,21917],[64055,22022],[64056,22120],[64057,22592],[64058,22696],[64059,23652],[64060,23662],[64061,24724],[64062,24936],[64063,24974],[64064,25074],[64065,25935],[64066,26082],[64067,26257],[64068,26757],[64069,28023],[64070,28186],[64071,28450],[64072,29038],[64073,29227],[64074,29730],[64075,30865],[64076,31038],[64077,31049],[64078,31048],[64079,31056],[64080,31062],[64081,31069],[64082,31117],[64083,31118],[64084,31296],[64085,31361],[64086,31680],[64087,32244],[64088,32265],[64089,32321],[64090,32626],[64091,32773],[64092,33261],[64093,33401],[64094,33401],[64095,33879],[64096,35088],[64097,35222],[64098,35585],[64099,35641],[64100,36051],[64101,36104],[64102,36790],[64103,36920],[64104,38627],[64105,38911],[64106,38971],[64107,24693],[64108,148206],[64109,33304],[64112,20006],[64113,20917],[64114,20840],[64115,20352],[64116,20805],[64117,20864],[64118,21191],[64119,21242],[64120,21917],[64121,21845],[64122,21913],[64123,21986],[64124,22618],[64125,22707],[64126,22852],[64127,22868],[64128,23138],[64129,23336],[64130,24274],[64131,24281],[64132,24425],[64133,24493],[64134,24792],[64135,24910],[64136,24840],[64137,24974],[64138,24928],[64139,25074],[64140,25140],[64141,25540],[64142,25628],[64143,25682],[64144,25942],[64145,26228],[64146,26391],[64147,26395],[64148,26454],[64149,27513],[64150,27578],[64151,27969],[64152,28379],[64153,28363],[64154,28450],[64155,28702],[64156,29038],[64157,30631],[64158,29237],[64159,29359],[64160,29482],[64161,29809],[64162,29958],[64163,30011],[64164,30237],[64165,30239],[64166,30410],[64167,30427],[64168,30452],[64169,30538],[64170,30528],[64171,30924],[64172,31409],[64173,31680],[64174,31867],[64175,32091],[64176,32244],[64177,32574],[64178,32773],[64179,33618],[64180,33775],[64181,34681],[64182,35137],[64183,35206],[64184,35222],[64185,35519],[64186,35576],[64187,35531],[64188,35585],[64189,35582],[64190,35565],[64191,35641],[64192,35722],[64193,36104],[64194,36664],[64195,36978],[64196,37273],[64197,37494],[64198,38524],[64199,38627],[64200,38742],[64201,38875],[64202,38911],[64203,38923],[64204,38971],[64205,39698],[64206,40860],[64207,141386],[64208,141380],[64209,144341],[64210,15261],[64211,16408],[64212,16441],[64213,152137],[64214,154832],[64215,163539],[64216,40771],[64217,40846],[64285,1497,1460],[64287,1522,1463],[64298,1513,1473],[64299,1513,1474],[64300,1513,1468,1473],[64301,1513,1468,1474],[64302,1488,1463],[64303,1488,1464],[64304,1488,1468],[64305,1489,1468],[64306,1490,1468],[64307,1491,1468],[64308,1492,1468],[64309,1493,1468],[64310,1494,1468],[64312,1496,1468],[64313,1497,1468],[64314,1498,1468],[64315,1499,1468],[64316,1500,1468],[64318,1502,1468],[64320,1504,1468],[64321,1505,1468],[64323,1507,1468],[64324,1508,1468],[64326,1510,1468],[64327,1511,1468],[64328,1512,1468],[64329,1513,1468],[64330,1514,1468],[64331,1493,1465],[64332,1489,1471],[64333,1499,1471],[64334,1508,1471],[69786,69785,69818],[69788,69787,69818],[69803,69797,69818],[69934,69937,69927],[69935,69938,69927],[70475,70471,70462],[70476,70471,70487],[70843,70841,70842],[70844,70841,70832],[70846,70841,70845],[71098,71096,71087],[71099,71097,71087],[71992,71989,71984],[119134,119127,119141],[119135,119128,119141],[119136,119128,119141,119150],[119137,119128,119141,119151],[119138,119128,119141,119152],[119139,119128,119141,119153],[119140,119128,119141,119154],[119227,119225,119141],[119228,119226,119141],[119229,119225,119141,119150],[119230,119226,119141,119150],[119231,119225,119141,119151],[119232,119226,119141,119151],[194560,20029],[194561,20024],[194562,20033],[194563,131362],[194564,20320],[194565,20398],[194566,20411],[194567,20482],[194568,20602],[194569,20633],[194570,20711],[194571,20687],[194572,13470],[194573,132666],[194574,20813],[194575,20820],[194576,20836],[194577,20855],[194578,132380],[194579,13497],[194580,20839],[194581,20877],[194582,132427],[194583,20887],[194584,20900],[194585,20172],[194586,20908],[194587,20917],[194588,168415],[194589,20981],[194590,20995],[194591,13535],[194592,21051],[194593,21062],[194594,21106],[194595,21111],[194596,13589],[194597,21191],[194598,21193],[194599,21220],[194600,21242],[194601,21253],[194602,21254],[194603,21271],[194604,21321],[194605,21329],[194606,21338],[194607,21363],[194608,21373],[194609,21375],[194610,21375],[194611,21375],[194612,133676],[194613,28784],[194614,21450],[194615,21471],[194616,133987],[194617,21483],[194618,21489],[194619,21510],[194620,21662],[194621,21560],[194622,21576],[194623,21608],[194624,21666],[194625,21750],[194626,21776],[194627,21843],[194628,21859],[194629,21892],[194630,21892],[194631,21913],[194632,21931],[194633,21939],[194634,21954],[194635,22294],[194636,22022],[194637,22295],[194638,22097],[194639,22132],[194640,20999],[194641,22766],[194642,22478],[194643,22516],[194644,22541],[194645,22411],[194646,22578],[194647,22577],[194648,22700],[194649,136420],[194650,22770],[194651,22775],[194652,22790],[194653,22810],[194654,22818],[194655,22882],[194656,136872],[194657,136938],[194658,23020],[194659,23067],[194660,23079],[194661,23000],[194662,23142],[194663,14062],[194664,14076],[194665,23304],[194666,23358],[194667,23358],[194668,137672],[194669,23491],[194670,23512],[194671,23527],[194672,23539],[194673,138008],[194674,23551],[194675,23558],[194676,24403],[194677,23586],[194678,14209],[194679,23648],[194680,23662],[194681,23744],[194682,23693],[194683,138724],[194684,23875],[194685,138726],[194686,23918],[194687,23915],[194688,23932],[194689,24033],[194690,24034],[194691,14383],[194692,24061],[194693,24104],[194694,24125],[194695,24169],[194696,14434],[194697,139651],[194698,14460],[194699,24240],[194700,24243],[194701,24246],[194702,24266],[194703,172946],[194704,24318],[194705,140081],[194706,140081],[194707,33281],[194708,24354],[194709,24354],[194710,14535],[194711,144056],[194712,156122],[194713,24418],[194714,24427],[194715,14563],[194716,24474],[194717,24525],[194718,24535],[194719,24569],[194720,24705],[194721,14650],[194722,14620],[194723,24724],[194724,141012],[194725,24775],[194726,24904],[194727,24908],[194728,24910],[194729,24908],[194730,24954],[194731,24974],[194732,25010],[194733,24996],[194734,25007],[194735,25054],[194736,25074],[194737,25078],[194738,25104],[194739,25115],[194740,25181],[194741,25265],[194742,25300],[194743,25424],[194744,142092],[194745,25405],[194746,25340],[194747,25448],[194748,25475],[194749,25572],[194750,142321],[194751,25634],[194752,25541],[194753,25513],[194754,14894],[194755,25705],[194756,25726],[194757,25757],[194758,25719],[194759,14956],[194760,25935],[194761,25964],[194762,143370],[194763,26083],[194764,26360],[194765,26185],[194766,15129],[194767,26257],[194768,15112],[194769,15076],[194770,20882],[194771,20885],[194772,26368],[194773,26268],[194774,32941],[194775,17369],[194776,26391],[194777,26395],[194778,26401],[194779,26462],[194780,26451],[194781,144323],[194782,15177],[194783,26618],[194784,26501],[194785,26706],[194786,26757],[194787,144493],[194788,26766],[194789,26655],[194790,26900],[194791,15261],[194792,26946],[194793,27043],[194794,27114],[194795,27304],[194796,145059],[194797,27355],[194798,15384],[194799,27425],[194800,145575],[194801,27476],[194802,15438],[194803,27506],[194804,27551],[194805,27578],[194806,27579],[194807,146061],[194808,138507],[194809,146170],[194810,27726],[194811,146620],[194812,27839],[194813,27853],[194814,27751],[194815,27926],[194816,27966],[194817,28023],[194818,27969],[194819,28009],[194820,28024],[194821,28037],[194822,146718],[194823,27956],[194824,28207],[194825,28270],[194826,15667],[194827,28363],[194828,28359],[194829,147153],[194830,28153],[194831,28526],[194832,147294],[194833,147342],[194834,28614],[194835,28729],[194836,28702],[194837,28699],[194838,15766],[194839,28746],[194840,28797],[194841,28791],[194842,28845],[194843,132389],[194844,28997],[194845,148067],[194846,29084],[194847,148395],[194848,29224],[194849,29237],[194850,29264],[194851,149000],[194852,29312],[194853,29333],[194854,149301],[194855,149524],[194856,29562],[194857,29579],[194858,16044],[194859,29605],[194860,16056],[194861,16056],[194862,29767],[194863,29788],[194864,29809],[194865,29829],[194866,29898],[194867,16155],[194868,29988],[194869,150582],[194870,30014],[194871,150674],[194872,30064],[194873,139679],[194874,30224],[194875,151457],[194876,151480],[194877,151620],[194878,16380],[194879,16392],[194880,30452],[194881,151795],[194882,151794],[194883,151833],[194884,151859],[194885,30494],[194886,30495],[194887,30495],[194888,30538],[194889,16441],[194890,30603],[194891,16454],[194892,16534],[194893,152605],[194894,30798],[194895,30860],[194896,30924],[194897,16611],[194898,153126],[194899,31062],[194900,153242],[194901,153285],[194902,31119],[194903,31211],[194904,16687],[194905,31296],[194906,31306],[194907,31311],[194908,153980],[194909,154279],[194910,154279],[194911,31470],[194912,16898],[194913,154539],[194914,31686],[194915,31689],[194916,16935],[194917,154752],[194918,31954],[194919,17056],[194920,31976],[194921,31971],[194922,32000],[194923,155526],[194924,32099],[194925,17153],[194926,32199],[194927,32258],[194928,32325],[194929,17204],[194930,156200],[194931,156231],[194932,17241],[194933,156377],[194934,32634],[194935,156478],[194936,32661],[194937,32762],[194938,32773],[194939,156890],[194940,156963],[194941,32864],[194942,157096],[194943,32880],[194944,144223],[194945,17365],[194946,32946],[194947,33027],[194948,17419],[194949,33086],[194950,23221],[194951,157607],[194952,157621],[194953,144275],[194954,144284],[194955,33281],[194956,33284],[194957,36766],[194958,17515],[194959,33425],[194960,33419],[194961,33437],[194962,21171],[194963,33457],[194964,33459],[194965,33469],[194966,33510],[194967,158524],[194968,33509],[194969,33565],[194970,33635],[194971,33709],[194972,33571],[194973,33725],[194974,33767],[194975,33879],[194976,33619],[194977,33738],[194978,33740],[194979,33756],[194980,158774],[194981,159083],[194982,158933],[194983,17707],[194984,34033],[194985,34035],[194986,34070],[194987,160714],[194988,34148],[194989,159532],[194990,17757],[194991,17761],[194992,159665],[194993,159954],[194994,17771],[194995,34384],[194996,34396],[194997,34407],[194998,34409],[194999,34473],[195000,34440],[195001,34574],[195002,34530],[195003,34681],[195004,34600],[195005,34667],[195006,34694],[195007,17879],[195008,34785],[195009,34817],[195010,17913],[195011,34912],[195012,34915],[195013,161383],[195014,35031],[195015,35038],[195016,17973],[195017,35066],[195018,13499],[195019,161966],[195020,162150],[195021,18110],[195022,18119],[195023,35488],[195024,35565],[195025,35722],[195026,35925],[195027,162984],[195028,36011],[195029,36033],[195030,36123],[195031,36215],[195032,163631],[195033,133124],[195034,36299],[195035,36284],[195036,36336],[195037,133342],[195038,36564],[195039,36664],[195040,165330],[195041,165357],[195042,37012],[195043,37105],[195044,37137],[195045,165678],[195046,37147],[195047,37432],[195048,37591],[195049,37592],[195050,37500],[195051,37881],[195052,37909],[195053,166906],[195054,38283],[195055,18837],[195056,38327],[195057,167287],[195058,18918],[195059,38595],[195060,23986],[195061,38691],[195062,168261],[195063,168474],[195064,19054],[195065,19062],[195066,38880],[195067,168970],[195068,19122],[195069,169110],[195070,38923],[195071,38923],[195072,38953],[195073,169398],[195074,39138],[195075,19251],[195076,39209],[195077,39335],[195078,39362],[195079,39422],[195080,19406],[195081,170800],[195082,39698],[195083,40000],[195084,40189],[195085,19662],[195086,19693],[195087,40295],[195088,172238],[195089,19704],[195090,172293],[195091,172558],[195092,172689],[195093,40635],[195094,19798],[195095,40697],[195096,40702],[195097,40709],[195098,40719],[195099,40726],[195100,40763],[195101,173568]],"composites":[192,193,194,195,196,197,199,200,201,202,203,204,205,206,207,209,210,211,212,213,214,217,218,219,220,221,224,225,226,227,228,229,231,232,233,234,235,236,237,238,239,241,242,243,244,245,246,249,250,251,252,253,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,296,297,298,299,300,301,302,303,304,308,309,310,311,313,314,315,316,317,318,323,324,325,326,327,328,332,333,334,335,336,337,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,416,417,431,432,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,478,479,480,481,482,483,486,487,488,489,490,491,492,493,494,495,496,500,501,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,542,543,550,551,552,553,554,555,556,557,558,559,560,561,562,563,901,902,904,905,906,908,910,911,912,938,939,940,941,942,943,944,970,971,972,973,974,979,980,1024,1025,1027,1031,1036,1037,1038,1049,1081,1104,1105,1107,1111,1116,1117,1118,1142,1143,1217,1218,1232,1233,1234,1235,1238,1239,1242,1243,1244,1245,1246,1247,1250,1251,1252,1253,1254,1255,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1272,1273,1570,1571,1572,1573,1574,1728,1730,1747,2345,2353,2356,2507,2508,2888,2891,2892,2964,3018,3019,3020,3144,3264,3271,3272,3274,3275,3402,3403,3404,3546,3548,3549,3550,4134,6918,6920,6922,6924,6926,6930,6971,6973,6976,6977,6979,7680,7681,7682,7683,7684,7685,7686,7687,7688,7689,7690,7691,7692,7693,7694,7695,7696,7697,7698,7699,7700,7701,7702,7703,7704,7705,7706,7707,7708,7709,7710,7711,7712,7713,7714,7715,7716,7717,7718,7719,7720,7721,7722,7723,7724,7725,7726,7727,7728,7729,7730,7731,7732,7733,7734,7735,7736,7737,7738,7739,7740,7741,7742,7743,7744,7745,7746,7747,7748,7749,7750,7751,7752,7753,7754,7755,7756,7757,7758,7759,7760,7761,7762,7763,7764,7765,7766,7767,7768,7769,7770,7771,7772,7773,7774,7775,7776,7777,7778,7779,7780,7781,7782,7783,7784,7785,7786,7787,7788,7789,7790,7791,7792,7793,7794,7795,7796,7797,7798,7799,7800,7801,7802,7803,7804,7805,7806,7807,7808,7809,7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823,7824,7825,7826,7827,7828,7829,7830,7831,7832,7833,7835,7840,7841,7842,7843,7844,7845,7846,7847,7848,7849,7850,7851,7852,7853,7854,7855,7856,7857,7858,7859,7860,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870,7871,7872,7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886,7887,7888,7889,7890,7891,7892,7893,7894,7895,7896,7897,7898,7899,7900,7901,7902,7903,7904,7905,7906,7907,7908,7909,7910,7911,7912,7913,7914,7915,7916,7917,7918,7919,7920,7921,7922,7923,7924,7925,7926,7927,7928,7929,7936,7937,7938,7939,7940,7941,7942,7943,7944,7945,7946,7947,7948,7949,7950,7951,7952,7953,7954,7955,7956,7957,7960,7961,7962,7963,7964,7965,7968,7969,7970,7971,7972,7973,7974,7975,7976,7977,7978,7979,7980,7981,7982,7983,7984,7985,7986,7987,7988,7989,7990,7991,7992,7993,7994,7995,7996,7997,7998,7999,8000,8001,8002,8003,8004,8005,8008,8009,8010,8011,8012,8013,8016,8017,8018,8019,8020,8021,8022,8023,8025,8027,8029,8031,8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047,8048,8050,8052,8054,8056,8058,8060,8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111,8112,8113,8114,8115,8116,8118,8119,8120,8121,8122,8124,8129,8130,8131,8132,8134,8135,8136,8138,8140,8141,8142,8143,8144,8145,8146,8150,8151,8152,8153,8154,8157,8158,8159,8160,8161,8162,8164,8165,8166,8167,8168,8169,8170,8172,8173,8178,8179,8180,8182,8183,8184,8186,8188,8602,8603,8622,8653,8654,8655,8708,8713,8716,8740,8742,8769,8772,8775,8777,8800,8802,8813,8814,8815,8816,8817,8820,8821,8824,8825,8832,8833,8836,8837,8840,8841,8876,8877,8878,8879,8928,8929,8930,8931,8938,8939,8940,8941,12364,12366,12368,12370,12372,12374,12376,12378,12380,12382,12384,12386,12389,12391,12393,12400,12401,12403,12404,12406,12407,12409,12410,12412,12413,12436,12446,12460,12462,12464,12466,12468,12470,12472,12474,12476,12478,12480,12482,12485,12487,12489,12496,12497,12499,12500,12502,12503,12505,12506,12508,12509,12532,12535,12536,12537,12538,12542,69786,69788,69803,69934,69935,70475,70476,70843,70844,70846,71098,71099,71992]} +{"unicodeVersion":"15.0.0","normVersion":"15.0.0","goVersion":"go1.25.3","assigned":[[0,887],[890,895],[900,906],[908,908],[910,929],[931,1327],[1329,1366],[1369,1418],[1421,1423],[1425,1479],[1488,1514],[1519,1524],[1536,1805],[1807,1866],[1869,1969],[1984,2042],[2045,2093],[2096,2110],[2112,2139],[2142,2142],[2144,2154],[2160,2190],[2192,2193],[2200,2435],[2437,2444],[2447,2448],[2451,2472],[2474,2480],[2482,2482],[2486,2489],[2492,2500],[2503,2504],[2507,2510],[2519,2519],[2524,2525],[2527,2531],[2534,2558],[2561,2563],[2565,2570],[2575,2576],[2579,2600],[2602,2608],[2610,2611],[2613,2614],[2616,2617],[2620,2620],[2622,2626],[2631,2632],[2635,2637],[2641,2641],[2649,2652],[2654,2654],[2662,2678],[2689,2691],[2693,2701],[2703,2705],[2707,2728],[2730,2736],[2738,2739],[2741,2745],[2748,2757],[2759,2761],[2763,2765],[2768,2768],[2784,2787],[2790,2801],[2809,2815],[2817,2819],[2821,2828],[2831,2832],[2835,2856],[2858,2864],[2866,2867],[2869,2873],[2876,2884],[2887,2888],[2891,2893],[2901,2903],[2908,2909],[2911,2915],[2918,2935],[2946,2947],[2949,2954],[2958,2960],[2962,2965],[2969,2970],[2972,2972],[2974,2975],[2979,2980],[2984,2986],[2990,3001],[3006,3010],[3014,3016],[3018,3021],[3024,3024],[3031,3031],[3046,3066],[3072,3084],[3086,3088],[3090,3112],[3114,3129],[3132,3140],[3142,3144],[3146,3149],[3157,3158],[3160,3162],[3165,3165],[3168,3171],[3174,3183],[3191,3212],[3214,3216],[3218,3240],[3242,3251],[3253,3257],[3260,3268],[3270,3272],[3274,3277],[3285,3286],[3293,3294],[3296,3299],[3302,3311],[3313,3315],[3328,3340],[3342,3344],[3346,3396],[3398,3400],[3402,3407],[3412,3427],[3430,3455],[3457,3459],[3461,3478],[3482,3505],[3507,3515],[3517,3517],[3520,3526],[3530,3530],[3535,3540],[3542,3542],[3544,3551],[3558,3567],[3570,3572],[3585,3642],[3647,3675],[3713,3714],[3716,3716],[3718,3722],[3724,3747],[3749,3749],[3751,3773],[3776,3780],[3782,3782],[3784,3790],[3792,3801],[3804,3807],[3840,3911],[3913,3948],[3953,3991],[3993,4028],[4030,4044],[4046,4058],[4096,4293],[4295,4295],[4301,4301],[4304,4680],[4682,4685],[4688,4694],[4696,4696],[4698,4701],[4704,4744],[4746,4749],[4752,4784],[4786,4789],[4792,4798],[4800,4800],[4802,4805],[4808,4822],[4824,4880],[4882,4885],[4888,4954],[4957,4988],[4992,5017],[5024,5109],[5112,5117],[5120,5788],[5792,5880],[5888,5909],[5919,5942],[5952,5971],[5984,5996],[5998,6000],[6002,6003],[6016,6109],[6112,6121],[6128,6137],[6144,6169],[6176,6264],[6272,6314],[6320,6389],[6400,6430],[6432,6443],[6448,6459],[6464,6464],[6468,6509],[6512,6516],[6528,6571],[6576,6601],[6608,6618],[6622,6683],[6686,6750],[6752,6780],[6783,6793],[6800,6809],[6816,6829],[6832,6862],[6912,6988],[6992,7038],[7040,7155],[7164,7223],[7227,7241],[7245,7304],[7312,7354],[7357,7367],[7376,7418],[7424,7957],[7960,7965],[7968,8005],[8008,8013],[8016,8023],[8025,8025],[8027,8027],[8029,8029],[8031,8061],[8064,8116],[8118,8132],[8134,8147],[8150,8155],[8157,8175],[8178,8180],[8182,8190],[8192,8292],[8294,8305],[8308,8334],[8336,8348],[8352,8384],[8400,8432],[8448,8587],[8592,9254],[9280,9290],[9312,11123],[11126,11157],[11159,11507],[11513,11557],[11559,11559],[11565,11565],[11568,11623],[11631,11632],[11647,11670],[11680,11686],[11688,11694],[11696,11702],[11704,11710],[11712,11718],[11720,11726],[11728,11734],[11736,11742],[11744,11869],[11904,11929],[11931,12019],[12032,12245],[12272,12283],[12288,12351],[12353,12438],[12441,12543],[12549,12591],[12593,12686],[12688,12771],[12784,12830],[12832,42124],[42128,42182],[42192,42539],[42560,42743],[42752,42954],[42960,42961],[42963,42963],[42965,42969],[42994,43052],[43056,43065],[43072,43127],[43136,43205],[43214,43225],[43232,43347],[43359,43388],[43392,43469],[43471,43481],[43486,43518],[43520,43574],[43584,43597],[43600,43609],[43612,43714],[43739,43766],[43777,43782],[43785,43790],[43793,43798],[43808,43814],[43816,43822],[43824,43883],[43888,44013],[44016,44025],[44032,55203],[55216,55238],[55243,55291],[55296,64109],[64112,64217],[64256,64262],[64275,64279],[64285,64310],[64312,64316],[64318,64318],[64320,64321],[64323,64324],[64326,64450],[64467,64911],[64914,64967],[64975,64975],[65008,65049],[65056,65106],[65108,65126],[65128,65131],[65136,65140],[65142,65276],[65279,65279],[65281,65470],[65474,65479],[65482,65487],[65490,65495],[65498,65500],[65504,65510],[65512,65518],[65529,65533],[65536,65547],[65549,65574],[65576,65594],[65596,65597],[65599,65613],[65616,65629],[65664,65786],[65792,65794],[65799,65843],[65847,65934],[65936,65948],[65952,65952],[66000,66045],[66176,66204],[66208,66256],[66272,66299],[66304,66339],[66349,66378],[66384,66426],[66432,66461],[66463,66499],[66504,66517],[66560,66717],[66720,66729],[66736,66771],[66776,66811],[66816,66855],[66864,66915],[66927,66938],[66940,66954],[66956,66962],[66964,66965],[66967,66977],[66979,66993],[66995,67001],[67003,67004],[67072,67382],[67392,67413],[67424,67431],[67456,67461],[67463,67504],[67506,67514],[67584,67589],[67592,67592],[67594,67637],[67639,67640],[67644,67644],[67647,67669],[67671,67742],[67751,67759],[67808,67826],[67828,67829],[67835,67867],[67871,67897],[67903,67903],[67968,68023],[68028,68047],[68050,68099],[68101,68102],[68108,68115],[68117,68119],[68121,68149],[68152,68154],[68159,68168],[68176,68184],[68192,68255],[68288,68326],[68331,68342],[68352,68405],[68409,68437],[68440,68466],[68472,68497],[68505,68508],[68521,68527],[68608,68680],[68736,68786],[68800,68850],[68858,68903],[68912,68921],[69216,69246],[69248,69289],[69291,69293],[69296,69297],[69373,69415],[69424,69465],[69488,69513],[69552,69579],[69600,69622],[69632,69709],[69714,69749],[69759,69826],[69837,69837],[69840,69864],[69872,69881],[69888,69940],[69942,69959],[69968,70006],[70016,70111],[70113,70132],[70144,70161],[70163,70209],[70272,70278],[70280,70280],[70282,70285],[70287,70301],[70303,70313],[70320,70378],[70384,70393],[70400,70403],[70405,70412],[70415,70416],[70419,70440],[70442,70448],[70450,70451],[70453,70457],[70459,70468],[70471,70472],[70475,70477],[70480,70480],[70487,70487],[70493,70499],[70502,70508],[70512,70516],[70656,70747],[70749,70753],[70784,70855],[70864,70873],[71040,71093],[71096,71133],[71168,71236],[71248,71257],[71264,71276],[71296,71353],[71360,71369],[71424,71450],[71453,71467],[71472,71494],[71680,71739],[71840,71922],[71935,71942],[71945,71945],[71948,71955],[71957,71958],[71960,71989],[71991,71992],[71995,72006],[72016,72025],[72096,72103],[72106,72151],[72154,72164],[72192,72263],[72272,72354],[72368,72440],[72448,72457],[72704,72712],[72714,72758],[72760,72773],[72784,72812],[72816,72847],[72850,72871],[72873,72886],[72960,72966],[72968,72969],[72971,73014],[73018,73018],[73020,73021],[73023,73031],[73040,73049],[73056,73061],[73063,73064],[73066,73102],[73104,73105],[73107,73112],[73120,73129],[73440,73464],[73472,73488],[73490,73530],[73534,73561],[73648,73648],[73664,73713],[73727,74649],[74752,74862],[74864,74868],[74880,75075],[77712,77810],[77824,78933],[82944,83526],[92160,92728],[92736,92766],[92768,92777],[92782,92862],[92864,92873],[92880,92909],[92912,92917],[92928,92997],[93008,93017],[93019,93025],[93027,93047],[93053,93071],[93760,93850],[93952,94026],[94031,94087],[94095,94111],[94176,94180],[94192,94193],[94208,100343],[100352,101589],[101632,101640],[110576,110579],[110581,110587],[110589,110590],[110592,110882],[110898,110898],[110928,110930],[110933,110933],[110948,110951],[110960,111355],[113664,113770],[113776,113788],[113792,113800],[113808,113817],[113820,113827],[118528,118573],[118576,118598],[118608,118723],[118784,119029],[119040,119078],[119081,119274],[119296,119365],[119488,119507],[119520,119539],[119552,119638],[119648,119672],[119808,119892],[119894,119964],[119966,119967],[119970,119970],[119973,119974],[119977,119980],[119982,119993],[119995,119995],[119997,120003],[120005,120069],[120071,120074],[120077,120084],[120086,120092],[120094,120121],[120123,120126],[120128,120132],[120134,120134],[120138,120144],[120146,120485],[120488,120779],[120782,121483],[121499,121503],[121505,121519],[122624,122654],[122661,122666],[122880,122886],[122888,122904],[122907,122913],[122915,122916],[122918,122922],[122928,122989],[123023,123023],[123136,123180],[123184,123197],[123200,123209],[123214,123215],[123536,123566],[123584,123641],[123647,123647],[124112,124153],[124896,124902],[124904,124907],[124909,124910],[124912,124926],[124928,125124],[125127,125142],[125184,125259],[125264,125273],[125278,125279],[126065,126132],[126209,126269],[126464,126467],[126469,126495],[126497,126498],[126500,126500],[126503,126503],[126505,126514],[126516,126519],[126521,126521],[126523,126523],[126530,126530],[126535,126535],[126537,126537],[126539,126539],[126541,126543],[126545,126546],[126548,126548],[126551,126551],[126553,126553],[126555,126555],[126557,126557],[126559,126559],[126561,126562],[126564,126564],[126567,126570],[126572,126578],[126580,126583],[126585,126588],[126590,126590],[126592,126601],[126603,126619],[126625,126627],[126629,126633],[126635,126651],[126704,126705],[126976,127019],[127024,127123],[127136,127150],[127153,127167],[127169,127183],[127185,127221],[127232,127405],[127462,127490],[127504,127547],[127552,127560],[127568,127569],[127584,127589],[127744,128727],[128732,128748],[128752,128764],[128768,128886],[128891,128985],[128992,129003],[129008,129008],[129024,129035],[129040,129095],[129104,129113],[129120,129159],[129168,129197],[129200,129201],[129280,129619],[129632,129645],[129648,129660],[129664,129672],[129680,129725],[129727,129733],[129742,129755],[129760,129768],[129776,129784],[129792,129938],[129940,129994],[130032,130041],[131072,173791],[173824,177977],[177984,178205],[178208,183969],[183984,191456],[194560,195101],[196608,201546],[201552,205743],[917505,917505],[917536,917631],[917760,917999],[983040,1048573],[1048576,1114109]],"format":[[173,173],[1536,1541],[1564,1564],[1757,1757],[1807,1807],[2192,2193],[2274,2274],[6158,6158],[8203,8207],[8234,8238],[8288,8292],[8294,8303],[65279,65279],[65529,65531],[69821,69821],[69837,69837],[78896,78911],[113824,113827],[119155,119162],[917505,917505],[917536,917631]],"combiningClass":[[768,230],[769,230],[770,230],[771,230],[772,230],[773,230],[774,230],[775,230],[776,230],[777,230],[778,230],[779,230],[780,230],[781,230],[782,230],[783,230],[784,230],[785,230],[786,230],[787,230],[788,230],[789,232],[790,220],[791,220],[792,220],[793,220],[794,232],[795,216],[796,220],[797,220],[798,220],[799,220],[800,220],[801,202],[802,202],[803,220],[804,220],[805,220],[806,220],[807,202],[808,202],[809,220],[810,220],[811,220],[812,220],[813,220],[814,220],[815,220],[816,220],[817,220],[818,220],[819,220],[820,1],[821,1],[822,1],[823,1],[824,1],[825,220],[826,220],[827,220],[828,220],[829,230],[830,230],[831,230],[832,230],[833,230],[834,230],[835,230],[836,230],[837,240],[838,230],[839,220],[840,220],[841,220],[842,230],[843,230],[844,230],[845,220],[846,220],[848,230],[849,230],[850,230],[851,220],[852,220],[853,220],[854,220],[855,230],[856,232],[857,220],[858,220],[859,230],[860,233],[861,234],[862,234],[863,233],[864,234],[865,234],[866,233],[867,230],[868,230],[869,230],[870,230],[871,230],[872,230],[873,230],[874,230],[875,230],[876,230],[877,230],[878,230],[879,230],[1155,230],[1156,230],[1157,230],[1158,230],[1159,230],[1425,220],[1426,230],[1427,230],[1428,230],[1429,230],[1430,220],[1431,230],[1432,230],[1433,230],[1434,222],[1435,220],[1436,230],[1437,230],[1438,230],[1439,230],[1440,230],[1441,230],[1442,220],[1443,220],[1444,220],[1445,220],[1446,220],[1447,220],[1448,230],[1449,230],[1450,220],[1451,230],[1452,230],[1453,222],[1454,228],[1455,230],[1456,10],[1457,11],[1458,12],[1459,13],[1460,14],[1461,15],[1462,16],[1463,17],[1464,18],[1465,19],[1466,19],[1467,20],[1468,21],[1469,22],[1471,23],[1473,24],[1474,25],[1476,230],[1477,220],[1479,18],[1552,230],[1553,230],[1554,230],[1555,230],[1556,230],[1557,230],[1558,230],[1559,230],[1560,30],[1561,31],[1562,32],[1611,27],[1612,28],[1613,29],[1614,30],[1615,31],[1616,32],[1617,33],[1618,34],[1619,230],[1620,230],[1621,220],[1622,220],[1623,230],[1624,230],[1625,230],[1626,230],[1627,230],[1628,220],[1629,230],[1630,230],[1631,220],[1648,35],[1750,230],[1751,230],[1752,230],[1753,230],[1754,230],[1755,230],[1756,230],[1759,230],[1760,230],[1761,230],[1762,230],[1763,220],[1764,230],[1767,230],[1768,230],[1770,220],[1771,230],[1772,230],[1773,220],[1809,36],[1840,230],[1841,220],[1842,230],[1843,230],[1844,220],[1845,230],[1846,230],[1847,220],[1848,220],[1849,220],[1850,230],[1851,220],[1852,220],[1853,230],[1854,220],[1855,230],[1856,230],[1857,230],[1858,220],[1859,230],[1860,220],[1861,230],[1862,220],[1863,230],[1864,220],[1865,230],[1866,230],[2027,230],[2028,230],[2029,230],[2030,230],[2031,230],[2032,230],[2033,230],[2034,220],[2035,230],[2045,220],[2070,230],[2071,230],[2072,230],[2073,230],[2075,230],[2076,230],[2077,230],[2078,230],[2079,230],[2080,230],[2081,230],[2082,230],[2083,230],[2085,230],[2086,230],[2087,230],[2089,230],[2090,230],[2091,230],[2092,230],[2093,230],[2137,220],[2138,220],[2139,220],[2200,230],[2201,220],[2202,220],[2203,220],[2204,230],[2205,230],[2206,230],[2207,230],[2250,230],[2251,230],[2252,230],[2253,230],[2254,230],[2255,220],[2256,220],[2257,220],[2258,220],[2259,220],[2260,230],[2261,230],[2262,230],[2263,230],[2264,230],[2265,230],[2266,230],[2267,230],[2268,230],[2269,230],[2270,230],[2271,230],[2272,230],[2273,230],[2275,220],[2276,230],[2277,230],[2278,220],[2279,230],[2280,230],[2281,220],[2282,230],[2283,230],[2284,230],[2285,220],[2286,220],[2287,220],[2288,27],[2289,28],[2290,29],[2291,230],[2292,230],[2293,230],[2294,220],[2295,230],[2296,230],[2297,220],[2298,220],[2299,230],[2300,230],[2301,230],[2302,230],[2303,230],[2364,7],[2381,9],[2385,230],[2386,220],[2387,230],[2388,230],[2492,7],[2509,9],[2558,230],[2620,7],[2637,9],[2748,7],[2765,9],[2876,7],[2893,9],[3021,9],[3132,7],[3149,9],[3157,84],[3158,91],[3260,7],[3277,9],[3387,9],[3388,9],[3405,9],[3530,9],[3640,103],[3641,103],[3642,9],[3656,107],[3657,107],[3658,107],[3659,107],[3768,118],[3769,118],[3770,9],[3784,122],[3785,122],[3786,122],[3787,122],[3864,220],[3865,220],[3893,220],[3895,220],[3897,216],[3953,129],[3954,130],[3956,132],[3962,130],[3963,130],[3964,130],[3965,130],[3968,130],[3970,230],[3971,230],[3972,9],[3974,230],[3975,230],[4038,220],[4151,7],[4153,9],[4154,9],[4237,220],[4957,230],[4958,230],[4959,230],[5908,9],[5909,9],[5940,9],[6098,9],[6109,230],[6313,228],[6457,222],[6458,230],[6459,220],[6679,230],[6680,220],[6752,9],[6773,230],[6774,230],[6775,230],[6776,230],[6777,230],[6778,230],[6779,230],[6780,230],[6783,220],[6832,230],[6833,230],[6834,230],[6835,230],[6836,230],[6837,220],[6838,220],[6839,220],[6840,220],[6841,220],[6842,220],[6843,230],[6844,230],[6845,220],[6847,220],[6848,220],[6849,230],[6850,230],[6851,220],[6852,220],[6853,230],[6854,230],[6855,230],[6856,230],[6857,230],[6858,220],[6859,230],[6860,230],[6861,230],[6862,230],[6964,7],[6980,9],[7019,230],[7020,220],[7021,230],[7022,230],[7023,230],[7024,230],[7025,230],[7026,230],[7027,230],[7082,9],[7083,9],[7142,7],[7154,9],[7155,9],[7223,7],[7376,230],[7377,230],[7378,230],[7380,1],[7381,220],[7382,220],[7383,220],[7384,220],[7385,220],[7386,230],[7387,230],[7388,220],[7389,220],[7390,220],[7391,220],[7392,230],[7394,1],[7395,1],[7396,1],[7397,1],[7398,1],[7399,1],[7400,1],[7405,220],[7412,230],[7416,230],[7417,230],[7616,230],[7617,230],[7618,220],[7619,230],[7620,230],[7621,230],[7622,230],[7623,230],[7624,230],[7625,230],[7626,220],[7627,230],[7628,230],[7629,234],[7630,214],[7631,220],[7632,202],[7633,230],[7634,230],[7635,230],[7636,230],[7637,230],[7638,230],[7639,230],[7640,230],[7641,230],[7642,230],[7643,230],[7644,230],[7645,230],[7646,230],[7647,230],[7648,230],[7649,230],[7650,230],[7651,230],[7652,230],[7653,230],[7654,230],[7655,230],[7656,230],[7657,230],[7658,230],[7659,230],[7660,230],[7661,230],[7662,230],[7663,230],[7664,230],[7665,230],[7666,230],[7667,230],[7668,230],[7669,230],[7670,232],[7671,228],[7672,228],[7673,220],[7674,218],[7675,230],[7676,233],[7677,220],[7678,230],[7679,220],[8400,230],[8401,230],[8402,1],[8403,1],[8404,230],[8405,230],[8406,230],[8407,230],[8408,1],[8409,1],[8410,1],[8411,230],[8412,230],[8417,230],[8421,1],[8422,1],[8423,230],[8424,220],[8425,230],[8426,1],[8427,1],[8428,220],[8429,220],[8430,220],[8431,220],[8432,230],[11503,230],[11504,230],[11505,230],[11647,9],[11744,230],[11745,230],[11746,230],[11747,230],[11748,230],[11749,230],[11750,230],[11751,230],[11752,230],[11753,230],[11754,230],[11755,230],[11756,230],[11757,230],[11758,230],[11759,230],[11760,230],[11761,230],[11762,230],[11763,230],[11764,230],[11765,230],[11766,230],[11767,230],[11768,230],[11769,230],[11770,230],[11771,230],[11772,230],[11773,230],[11774,230],[11775,230],[12330,218],[12331,228],[12332,232],[12333,222],[12334,224],[12335,224],[12441,8],[12442,8],[42607,230],[42612,230],[42613,230],[42614,230],[42615,230],[42616,230],[42617,230],[42618,230],[42619,230],[42620,230],[42621,230],[42654,230],[42655,230],[42736,230],[42737,230],[43014,9],[43052,9],[43204,9],[43232,230],[43233,230],[43234,230],[43235,230],[43236,230],[43237,230],[43238,230],[43239,230],[43240,230],[43241,230],[43242,230],[43243,230],[43244,230],[43245,230],[43246,230],[43247,230],[43248,230],[43249,230],[43307,220],[43308,220],[43309,220],[43347,9],[43443,7],[43456,9],[43696,230],[43698,230],[43699,230],[43700,220],[43703,230],[43704,230],[43710,230],[43711,230],[43713,230],[43766,9],[44013,9],[64286,26],[65056,230],[65057,230],[65058,230],[65059,230],[65060,230],[65061,230],[65062,230],[65063,220],[65064,220],[65065,220],[65066,220],[65067,220],[65068,220],[65069,220],[65070,230],[65071,230],[66045,220],[66272,220],[66422,230],[66423,230],[66424,230],[66425,230],[66426,230],[68109,220],[68111,230],[68152,230],[68153,1],[68154,220],[68159,9],[68325,230],[68326,220],[68900,230],[68901,230],[68902,230],[68903,230],[69291,230],[69292,230],[69373,220],[69374,220],[69375,220],[69446,220],[69447,220],[69448,230],[69449,230],[69450,230],[69451,220],[69452,230],[69453,220],[69454,220],[69455,220],[69456,220],[69506,230],[69507,220],[69508,230],[69509,220],[69702,9],[69744,9],[69759,9],[69817,9],[69818,7],[69888,230],[69889,230],[69890,230],[69939,9],[69940,9],[70003,7],[70080,9],[70090,7],[70197,9],[70198,7],[70377,7],[70378,9],[70459,7],[70460,7],[70477,9],[70502,230],[70503,230],[70504,230],[70505,230],[70506,230],[70507,230],[70508,230],[70512,230],[70513,230],[70514,230],[70515,230],[70516,230],[70722,9],[70726,7],[70750,230],[70850,9],[70851,7],[71103,9],[71104,7],[71231,9],[71350,9],[71351,7],[71467,9],[71737,9],[71738,7],[71997,9],[71998,9],[72003,7],[72160,9],[72244,9],[72263,9],[72345,9],[72767,9],[73026,7],[73028,9],[73029,9],[73111,9],[73537,9],[73538,9],[92912,1],[92913,1],[92914,1],[92915,1],[92916,1],[92976,230],[92977,230],[92978,230],[92979,230],[92980,230],[92981,230],[92982,230],[94192,6],[94193,6],[113822,1],[119141,216],[119142,216],[119143,1],[119144,1],[119145,1],[119149,226],[119150,216],[119151,216],[119152,216],[119153,216],[119154,216],[119163,220],[119164,220],[119165,220],[119166,220],[119167,220],[119168,220],[119169,220],[119170,220],[119173,230],[119174,230],[119175,230],[119176,230],[119177,230],[119178,220],[119179,220],[119210,230],[119211,230],[119212,230],[119213,230],[119362,230],[119363,230],[119364,230],[122880,230],[122881,230],[122882,230],[122883,230],[122884,230],[122885,230],[122886,230],[122888,230],[122889,230],[122890,230],[122891,230],[122892,230],[122893,230],[122894,230],[122895,230],[122896,230],[122897,230],[122898,230],[122899,230],[122900,230],[122901,230],[122902,230],[122903,230],[122904,230],[122907,230],[122908,230],[122909,230],[122910,230],[122911,230],[122912,230],[122913,230],[122915,230],[122916,230],[122918,230],[122919,230],[122920,230],[122921,230],[122922,230],[123023,230],[123184,230],[123185,230],[123186,230],[123187,230],[123188,230],[123189,230],[123190,230],[123566,230],[123628,230],[123629,230],[123630,230],[123631,230],[124140,232],[124141,232],[124142,220],[124143,230],[125136,220],[125137,220],[125138,220],[125139,220],[125140,220],[125141,220],[125142,220],[125252,230],[125253,230],[125254,230],[125255,230],[125256,230],[125257,230],[125258,7]],"decompositions":[[192,65,768],[193,65,769],[194,65,770],[195,65,771],[196,65,776],[197,65,778],[199,67,807],[200,69,768],[201,69,769],[202,69,770],[203,69,776],[204,73,768],[205,73,769],[206,73,770],[207,73,776],[209,78,771],[210,79,768],[211,79,769],[212,79,770],[213,79,771],[214,79,776],[217,85,768],[218,85,769],[219,85,770],[220,85,776],[221,89,769],[224,97,768],[225,97,769],[226,97,770],[227,97,771],[228,97,776],[229,97,778],[231,99,807],[232,101,768],[233,101,769],[234,101,770],[235,101,776],[236,105,768],[237,105,769],[238,105,770],[239,105,776],[241,110,771],[242,111,768],[243,111,769],[244,111,770],[245,111,771],[246,111,776],[249,117,768],[250,117,769],[251,117,770],[252,117,776],[253,121,769],[255,121,776],[256,65,772],[257,97,772],[258,65,774],[259,97,774],[260,65,808],[261,97,808],[262,67,769],[263,99,769],[264,67,770],[265,99,770],[266,67,775],[267,99,775],[268,67,780],[269,99,780],[270,68,780],[271,100,780],[274,69,772],[275,101,772],[276,69,774],[277,101,774],[278,69,775],[279,101,775],[280,69,808],[281,101,808],[282,69,780],[283,101,780],[284,71,770],[285,103,770],[286,71,774],[287,103,774],[288,71,775],[289,103,775],[290,71,807],[291,103,807],[292,72,770],[293,104,770],[296,73,771],[297,105,771],[298,73,772],[299,105,772],[300,73,774],[301,105,774],[302,73,808],[303,105,808],[304,73,775],[308,74,770],[309,106,770],[310,75,807],[311,107,807],[313,76,769],[314,108,769],[315,76,807],[316,108,807],[317,76,780],[318,108,780],[323,78,769],[324,110,769],[325,78,807],[326,110,807],[327,78,780],[328,110,780],[332,79,772],[333,111,772],[334,79,774],[335,111,774],[336,79,779],[337,111,779],[340,82,769],[341,114,769],[342,82,807],[343,114,807],[344,82,780],[345,114,780],[346,83,769],[347,115,769],[348,83,770],[349,115,770],[350,83,807],[351,115,807],[352,83,780],[353,115,780],[354,84,807],[355,116,807],[356,84,780],[357,116,780],[360,85,771],[361,117,771],[362,85,772],[363,117,772],[364,85,774],[365,117,774],[366,85,778],[367,117,778],[368,85,779],[369,117,779],[370,85,808],[371,117,808],[372,87,770],[373,119,770],[374,89,770],[375,121,770],[376,89,776],[377,90,769],[378,122,769],[379,90,775],[380,122,775],[381,90,780],[382,122,780],[416,79,795],[417,111,795],[431,85,795],[432,117,795],[461,65,780],[462,97,780],[463,73,780],[464,105,780],[465,79,780],[466,111,780],[467,85,780],[468,117,780],[469,220,772],[470,252,772],[471,220,769],[472,252,769],[473,220,780],[474,252,780],[475,220,768],[476,252,768],[478,196,772],[479,228,772],[480,550,772],[481,551,772],[482,198,772],[483,230,772],[486,71,780],[487,103,780],[488,75,780],[489,107,780],[490,79,808],[491,111,808],[492,490,772],[493,491,772],[494,439,780],[495,658,780],[496,106,780],[500,71,769],[501,103,769],[504,78,768],[505,110,768],[506,197,769],[507,229,769],[508,198,769],[509,230,769],[510,216,769],[511,248,769],[512,65,783],[513,97,783],[514,65,785],[515,97,785],[516,69,783],[517,101,783],[518,69,785],[519,101,785],[520,73,783],[521,105,783],[522,73,785],[523,105,785],[524,79,783],[525,111,783],[526,79,785],[527,111,785],[528,82,783],[529,114,783],[530,82,785],[531,114,785],[532,85,783],[533,117,783],[534,85,785],[535,117,785],[536,83,806],[537,115,806],[538,84,806],[539,116,806],[542,72,780],[543,104,780],[550,65,775],[551,97,775],[552,69,807],[553,101,807],[554,214,772],[555,246,772],[556,213,772],[557,245,772],[558,79,775],[559,111,775],[560,558,772],[561,559,772],[562,89,772],[563,121,772],[832,768],[833,769],[835,787],[836,776,769],[884,697],[894,59],[901,168,769],[902,913,769],[903,183],[904,917,769],[905,919,769],[906,921,769],[908,927,769],[910,933,769],[911,937,769],[912,970,769],[938,921,776],[939,933,776],[940,945,769],[941,949,769],[942,951,769],[943,953,769],[944,971,769],[970,953,776],[971,965,776],[972,959,769],[973,965,769],[974,969,769],[979,978,769],[980,978,776],[1024,1045,768],[1025,1045,776],[1027,1043,769],[1031,1030,776],[1036,1050,769],[1037,1048,768],[1038,1059,774],[1049,1048,774],[1081,1080,774],[1104,1077,768],[1105,1077,776],[1107,1075,769],[1111,1110,776],[1116,1082,769],[1117,1080,768],[1118,1091,774],[1142,1140,783],[1143,1141,783],[1217,1046,774],[1218,1078,774],[1232,1040,774],[1233,1072,774],[1234,1040,776],[1235,1072,776],[1238,1045,774],[1239,1077,774],[1242,1240,776],[1243,1241,776],[1244,1046,776],[1245,1078,776],[1246,1047,776],[1247,1079,776],[1250,1048,772],[1251,1080,772],[1252,1048,776],[1253,1080,776],[1254,1054,776],[1255,1086,776],[1258,1256,776],[1259,1257,776],[1260,1069,776],[1261,1101,776],[1262,1059,772],[1263,1091,772],[1264,1059,776],[1265,1091,776],[1266,1059,779],[1267,1091,779],[1268,1063,776],[1269,1095,776],[1272,1067,776],[1273,1099,776],[1570,1575,1619],[1571,1575,1620],[1572,1608,1620],[1573,1575,1621],[1574,1610,1620],[1728,1749,1620],[1730,1729,1620],[1747,1746,1620],[2345,2344,2364],[2353,2352,2364],[2356,2355,2364],[2392,2325,2364],[2393,2326,2364],[2394,2327,2364],[2395,2332,2364],[2396,2337,2364],[2397,2338,2364],[2398,2347,2364],[2399,2351,2364],[2507,2503,2494],[2508,2503,2519],[2524,2465,2492],[2525,2466,2492],[2527,2479,2492],[2611,2610,2620],[2614,2616,2620],[2649,2582,2620],[2650,2583,2620],[2651,2588,2620],[2654,2603,2620],[2888,2887,2902],[2891,2887,2878],[2892,2887,2903],[2908,2849,2876],[2909,2850,2876],[2964,2962,3031],[3018,3014,3006],[3019,3015,3006],[3020,3014,3031],[3144,3142,3158],[3264,3263,3285],[3271,3270,3285],[3272,3270,3286],[3274,3270,3266],[3275,3274,3285],[3402,3398,3390],[3403,3399,3390],[3404,3398,3415],[3546,3545,3530],[3548,3545,3535],[3549,3548,3530],[3550,3545,3551],[3907,3906,4023],[3917,3916,4023],[3922,3921,4023],[3927,3926,4023],[3932,3931,4023],[3945,3904,4021],[3955,3953,3954],[3957,3953,3956],[3958,4018,3968],[3960,4019,3968],[3969,3953,3968],[3987,3986,4023],[3997,3996,4023],[4002,4001,4023],[4007,4006,4023],[4012,4011,4023],[4025,3984,4021],[4134,4133,4142],[6918,6917,6965],[6920,6919,6965],[6922,6921,6965],[6924,6923,6965],[6926,6925,6965],[6930,6929,6965],[6971,6970,6965],[6973,6972,6965],[6976,6974,6965],[6977,6975,6965],[6979,6978,6965],[7680,65,805],[7681,97,805],[7682,66,775],[7683,98,775],[7684,66,803],[7685,98,803],[7686,66,817],[7687,98,817],[7688,199,769],[7689,231,769],[7690,68,775],[7691,100,775],[7692,68,803],[7693,100,803],[7694,68,817],[7695,100,817],[7696,68,807],[7697,100,807],[7698,68,813],[7699,100,813],[7700,274,768],[7701,275,768],[7702,274,769],[7703,275,769],[7704,69,813],[7705,101,813],[7706,69,816],[7707,101,816],[7708,552,774],[7709,553,774],[7710,70,775],[7711,102,775],[7712,71,772],[7713,103,772],[7714,72,775],[7715,104,775],[7716,72,803],[7717,104,803],[7718,72,776],[7719,104,776],[7720,72,807],[7721,104,807],[7722,72,814],[7723,104,814],[7724,73,816],[7725,105,816],[7726,207,769],[7727,239,769],[7728,75,769],[7729,107,769],[7730,75,803],[7731,107,803],[7732,75,817],[7733,107,817],[7734,76,803],[7735,108,803],[7736,7734,772],[7737,7735,772],[7738,76,817],[7739,108,817],[7740,76,813],[7741,108,813],[7742,77,769],[7743,109,769],[7744,77,775],[7745,109,775],[7746,77,803],[7747,109,803],[7748,78,775],[7749,110,775],[7750,78,803],[7751,110,803],[7752,78,817],[7753,110,817],[7754,78,813],[7755,110,813],[7756,213,769],[7757,245,769],[7758,213,776],[7759,245,776],[7760,332,768],[7761,333,768],[7762,332,769],[7763,333,769],[7764,80,769],[7765,112,769],[7766,80,775],[7767,112,775],[7768,82,775],[7769,114,775],[7770,82,803],[7771,114,803],[7772,7770,772],[7773,7771,772],[7774,82,817],[7775,114,817],[7776,83,775],[7777,115,775],[7778,83,803],[7779,115,803],[7780,346,775],[7781,347,775],[7782,352,775],[7783,353,775],[7784,7778,775],[7785,7779,775],[7786,84,775],[7787,116,775],[7788,84,803],[7789,116,803],[7790,84,817],[7791,116,817],[7792,84,813],[7793,116,813],[7794,85,804],[7795,117,804],[7796,85,816],[7797,117,816],[7798,85,813],[7799,117,813],[7800,360,769],[7801,361,769],[7802,362,776],[7803,363,776],[7804,86,771],[7805,118,771],[7806,86,803],[7807,118,803],[7808,87,768],[7809,119,768],[7810,87,769],[7811,119,769],[7812,87,776],[7813,119,776],[7814,87,775],[7815,119,775],[7816,87,803],[7817,119,803],[7818,88,775],[7819,120,775],[7820,88,776],[7821,120,776],[7822,89,775],[7823,121,775],[7824,90,770],[7825,122,770],[7826,90,803],[7827,122,803],[7828,90,817],[7829,122,817],[7830,104,817],[7831,116,776],[7832,119,778],[7833,121,778],[7835,383,775],[7840,65,803],[7841,97,803],[7842,65,777],[7843,97,777],[7844,194,769],[7845,226,769],[7846,194,768],[7847,226,768],[7848,194,777],[7849,226,777],[7850,194,771],[7851,226,771],[7852,7840,770],[7853,7841,770],[7854,258,769],[7855,259,769],[7856,258,768],[7857,259,768],[7858,258,777],[7859,259,777],[7860,258,771],[7861,259,771],[7862,7840,774],[7863,7841,774],[7864,69,803],[7865,101,803],[7866,69,777],[7867,101,777],[7868,69,771],[7869,101,771],[7870,202,769],[7871,234,769],[7872,202,768],[7873,234,768],[7874,202,777],[7875,234,777],[7876,202,771],[7877,234,771],[7878,7864,770],[7879,7865,770],[7880,73,777],[7881,105,777],[7882,73,803],[7883,105,803],[7884,79,803],[7885,111,803],[7886,79,777],[7887,111,777],[7888,212,769],[7889,244,769],[7890,212,768],[7891,244,768],[7892,212,777],[7893,244,777],[7894,212,771],[7895,244,771],[7896,7884,770],[7897,7885,770],[7898,416,769],[7899,417,769],[7900,416,768],[7901,417,768],[7902,416,777],[7903,417,777],[7904,416,771],[7905,417,771],[7906,416,803],[7907,417,803],[7908,85,803],[7909,117,803],[7910,85,777],[7911,117,777],[7912,431,769],[7913,432,769],[7914,431,768],[7915,432,768],[7916,431,777],[7917,432,777],[7918,431,771],[7919,432,771],[7920,431,803],[7921,432,803],[7922,89,768],[7923,121,768],[7924,89,803],[7925,121,803],[7926,89,777],[7927,121,777],[7928,89,771],[7929,121,771],[7936,945,787],[7937,945,788],[7938,7936,768],[7939,7937,768],[7940,7936,769],[7941,7937,769],[7942,7936,834],[7943,7937,834],[7944,913,787],[7945,913,788],[7946,7944,768],[7947,7945,768],[7948,7944,769],[7949,7945,769],[7950,7944,834],[7951,7945,834],[7952,949,787],[7953,949,788],[7954,7952,768],[7955,7953,768],[7956,7952,769],[7957,7953,769],[7960,917,787],[7961,917,788],[7962,7960,768],[7963,7961,768],[7964,7960,769],[7965,7961,769],[7968,951,787],[7969,951,788],[7970,7968,768],[7971,7969,768],[7972,7968,769],[7973,7969,769],[7974,7968,834],[7975,7969,834],[7976,919,787],[7977,919,788],[7978,7976,768],[7979,7977,768],[7980,7976,769],[7981,7977,769],[7982,7976,834],[7983,7977,834],[7984,953,787],[7985,953,788],[7986,7984,768],[7987,7985,768],[7988,7984,769],[7989,7985,769],[7990,7984,834],[7991,7985,834],[7992,921,787],[7993,921,788],[7994,7992,768],[7995,7993,768],[7996,7992,769],[7997,7993,769],[7998,7992,834],[7999,7993,834],[8000,959,787],[8001,959,788],[8002,8000,768],[8003,8001,768],[8004,8000,769],[8005,8001,769],[8008,927,787],[8009,927,788],[8010,8008,768],[8011,8009,768],[8012,8008,769],[8013,8009,769],[8016,965,787],[8017,965,788],[8018,8016,768],[8019,8017,768],[8020,8016,769],[8021,8017,769],[8022,8016,834],[8023,8017,834],[8025,933,788],[8027,8025,768],[8029,8025,769],[8031,8025,834],[8032,969,787],[8033,969,788],[8034,8032,768],[8035,8033,768],[8036,8032,769],[8037,8033,769],[8038,8032,834],[8039,8033,834],[8040,937,787],[8041,937,788],[8042,8040,768],[8043,8041,768],[8044,8040,769],[8045,8041,769],[8046,8040,834],[8047,8041,834],[8048,945,768],[8049,945,769],[8050,949,768],[8051,949,769],[8052,951,768],[8053,951,769],[8054,953,768],[8055,953,769],[8056,959,768],[8057,959,769],[8058,965,768],[8059,965,769],[8060,969,768],[8061,969,769],[8064,7936,837],[8065,7937,837],[8066,7938,837],[8067,7939,837],[8068,7940,837],[8069,7941,837],[8070,7942,837],[8071,7943,837],[8072,7944,837],[8073,7945,837],[8074,7946,837],[8075,7947,837],[8076,7948,837],[8077,7949,837],[8078,7950,837],[8079,7951,837],[8080,7968,837],[8081,7969,837],[8082,7970,837],[8083,7971,837],[8084,7972,837],[8085,7973,837],[8086,7974,837],[8087,7975,837],[8088,7976,837],[8089,7977,837],[8090,7978,837],[8091,7979,837],[8092,7980,837],[8093,7981,837],[8094,7982,837],[8095,7983,837],[8096,8032,837],[8097,8033,837],[8098,8034,837],[8099,8035,837],[8100,8036,837],[8101,8037,837],[8102,8038,837],[8103,8039,837],[8104,8040,837],[8105,8041,837],[8106,8042,837],[8107,8043,837],[8108,8044,837],[8109,8045,837],[8110,8046,837],[8111,8047,837],[8112,945,774],[8113,945,772],[8114,8048,837],[8115,945,837],[8116,940,837],[8118,945,834],[8119,8118,837],[8120,913,774],[8121,913,772],[8122,913,768],[8123,913,769],[8124,913,837],[8126,953],[8129,168,834],[8130,8052,837],[8131,951,837],[8132,942,837],[8134,951,834],[8135,8134,837],[8136,917,768],[8137,917,769],[8138,919,768],[8139,919,769],[8140,919,837],[8141,8127,768],[8142,8127,769],[8143,8127,834],[8144,953,774],[8145,953,772],[8146,970,768],[8147,953,776,769],[8150,953,834],[8151,970,834],[8152,921,774],[8153,921,772],[8154,921,768],[8155,921,769],[8157,8190,768],[8158,8190,769],[8159,8190,834],[8160,965,774],[8161,965,772],[8162,971,768],[8163,965,776,769],[8164,961,787],[8165,961,788],[8166,965,834],[8167,971,834],[8168,933,774],[8169,933,772],[8170,933,768],[8171,933,769],[8172,929,788],[8173,168,768],[8174,168,769],[8175,96],[8178,8060,837],[8179,969,837],[8180,974,837],[8182,969,834],[8183,8182,837],[8184,927,768],[8185,927,769],[8186,937,768],[8187,937,769],[8188,937,837],[8189,180],[8192,8194],[8193,8195],[8486,937],[8490,75],[8491,65,778],[8602,8592,824],[8603,8594,824],[8622,8596,824],[8653,8656,824],[8654,8660,824],[8655,8658,824],[8708,8707,824],[8713,8712,824],[8716,8715,824],[8740,8739,824],[8742,8741,824],[8769,8764,824],[8772,8771,824],[8775,8773,824],[8777,8776,824],[8800,61,824],[8802,8801,824],[8813,8781,824],[8814,60,824],[8815,62,824],[8816,8804,824],[8817,8805,824],[8820,8818,824],[8821,8819,824],[8824,8822,824],[8825,8823,824],[8832,8826,824],[8833,8827,824],[8836,8834,824],[8837,8835,824],[8840,8838,824],[8841,8839,824],[8876,8866,824],[8877,8872,824],[8878,8873,824],[8879,8875,824],[8928,8828,824],[8929,8829,824],[8930,8849,824],[8931,8850,824],[8938,8882,824],[8939,8883,824],[8940,8884,824],[8941,8885,824],[9001,12296],[9002,12297],[10972,10973,824],[12364,12363,12441],[12366,12365,12441],[12368,12367,12441],[12370,12369,12441],[12372,12371,12441],[12374,12373,12441],[12376,12375,12441],[12378,12377,12441],[12380,12379,12441],[12382,12381,12441],[12384,12383,12441],[12386,12385,12441],[12389,12388,12441],[12391,12390,12441],[12393,12392,12441],[12400,12399,12441],[12401,12399,12442],[12403,12402,12441],[12404,12402,12442],[12406,12405,12441],[12407,12405,12442],[12409,12408,12441],[12410,12408,12442],[12412,12411,12441],[12413,12411,12442],[12436,12358,12441],[12446,12445,12441],[12460,12459,12441],[12462,12461,12441],[12464,12463,12441],[12466,12465,12441],[12468,12467,12441],[12470,12469,12441],[12472,12471,12441],[12474,12473,12441],[12476,12475,12441],[12478,12477,12441],[12480,12479,12441],[12482,12481,12441],[12485,12484,12441],[12487,12486,12441],[12489,12488,12441],[12496,12495,12441],[12497,12495,12442],[12499,12498,12441],[12500,12498,12442],[12502,12501,12441],[12503,12501,12442],[12505,12504,12441],[12506,12504,12442],[12508,12507,12441],[12509,12507,12442],[12532,12454,12441],[12535,12527,12441],[12536,12528,12441],[12537,12529,12441],[12538,12530,12441],[12542,12541,12441],[63744,35912],[63745,26356],[63746,36554],[63747,36040],[63748,28369],[63749,20018],[63750,21477],[63751,40860],[63752,40860],[63753,22865],[63754,37329],[63755,21895],[63756,22856],[63757,25078],[63758,30313],[63759,32645],[63760,34367],[63761,34746],[63762,35064],[63763,37007],[63764,27138],[63765,27931],[63766,28889],[63767,29662],[63768,33853],[63769,37226],[63770,39409],[63771,20098],[63772,21365],[63773,27396],[63774,29211],[63775,34349],[63776,40478],[63777,23888],[63778,28651],[63779,34253],[63780,35172],[63781,25289],[63782,33240],[63783,34847],[63784,24266],[63785,26391],[63786,28010],[63787,29436],[63788,37070],[63789,20358],[63790,20919],[63791,21214],[63792,25796],[63793,27347],[63794,29200],[63795,30439],[63796,32769],[63797,34310],[63798,34396],[63799,36335],[63800,38706],[63801,39791],[63802,40442],[63803,30860],[63804,31103],[63805,32160],[63806,33737],[63807,37636],[63808,40575],[63809,35542],[63810,22751],[63811,24324],[63812,31840],[63813,32894],[63814,29282],[63815,30922],[63816,36034],[63817,38647],[63818,22744],[63819,23650],[63820,27155],[63821,28122],[63822,28431],[63823,32047],[63824,32311],[63825,38475],[63826,21202],[63827,32907],[63828,20956],[63829,20940],[63830,31260],[63831,32190],[63832,33777],[63833,38517],[63834,35712],[63835,25295],[63836,27138],[63837,35582],[63838,20025],[63839,23527],[63840,24594],[63841,29575],[63842,30064],[63843,21271],[63844,30971],[63845,20415],[63846,24489],[63847,19981],[63848,27852],[63849,25976],[63850,32034],[63851,21443],[63852,22622],[63853,30465],[63854,33865],[63855,35498],[63856,27578],[63857,36784],[63858,27784],[63859,25342],[63860,33509],[63861,25504],[63862,30053],[63863,20142],[63864,20841],[63865,20937],[63866,26753],[63867,31975],[63868,33391],[63869,35538],[63870,37327],[63871,21237],[63872,21570],[63873,22899],[63874,24300],[63875,26053],[63876,28670],[63877,31018],[63878,38317],[63879,39530],[63880,40599],[63881,40654],[63882,21147],[63883,26310],[63884,27511],[63885,36706],[63886,24180],[63887,24976],[63888,25088],[63889,25754],[63890,28451],[63891,29001],[63892,29833],[63893,31178],[63894,32244],[63895,32879],[63896,36646],[63897,34030],[63898,36899],[63899,37706],[63900,21015],[63901,21155],[63902,21693],[63903,28872],[63904,35010],[63905,35498],[63906,24265],[63907,24565],[63908,25467],[63909,27566],[63910,31806],[63911,29557],[63912,20196],[63913,22265],[63914,23527],[63915,23994],[63916,24604],[63917,29618],[63918,29801],[63919,32666],[63920,32838],[63921,37428],[63922,38646],[63923,38728],[63924,38936],[63925,20363],[63926,31150],[63927,37300],[63928,38584],[63929,24801],[63930,20102],[63931,20698],[63932,23534],[63933,23615],[63934,26009],[63935,27138],[63936,29134],[63937,30274],[63938,34044],[63939,36988],[63940,40845],[63941,26248],[63942,38446],[63943,21129],[63944,26491],[63945,26611],[63946,27969],[63947,28316],[63948,29705],[63949,30041],[63950,30827],[63951,32016],[63952,39006],[63953,20845],[63954,25134],[63955,38520],[63956,20523],[63957,23833],[63958,28138],[63959,36650],[63960,24459],[63961,24900],[63962,26647],[63963,29575],[63964,38534],[63965,21033],[63966,21519],[63967,23653],[63968,26131],[63969,26446],[63970,26792],[63971,27877],[63972,29702],[63973,30178],[63974,32633],[63975,35023],[63976,35041],[63977,37324],[63978,38626],[63979,21311],[63980,28346],[63981,21533],[63982,29136],[63983,29848],[63984,34298],[63985,38563],[63986,40023],[63987,40607],[63988,26519],[63989,28107],[63990,33256],[63991,31435],[63992,31520],[63993,31890],[63994,29376],[63995,28825],[63996,35672],[63997,20160],[63998,33590],[63999,21050],[64000,20999],[64001,24230],[64002,25299],[64003,31958],[64004,23429],[64005,27934],[64006,26292],[64007,36667],[64008,34892],[64009,38477],[64010,35211],[64011,24275],[64012,20800],[64013,21952],[64016,22618],[64018,26228],[64021,20958],[64022,29482],[64023,30410],[64024,31036],[64025,31070],[64026,31077],[64027,31119],[64028,38742],[64029,31934],[64030,32701],[64032,34322],[64034,35576],[64037,36920],[64038,37117],[64042,39151],[64043,39164],[64044,39208],[64045,40372],[64046,37086],[64047,38583],[64048,20398],[64049,20711],[64050,20813],[64051,21193],[64052,21220],[64053,21329],[64054,21917],[64055,22022],[64056,22120],[64057,22592],[64058,22696],[64059,23652],[64060,23662],[64061,24724],[64062,24936],[64063,24974],[64064,25074],[64065,25935],[64066,26082],[64067,26257],[64068,26757],[64069,28023],[64070,28186],[64071,28450],[64072,29038],[64073,29227],[64074,29730],[64075,30865],[64076,31038],[64077,31049],[64078,31048],[64079,31056],[64080,31062],[64081,31069],[64082,31117],[64083,31118],[64084,31296],[64085,31361],[64086,31680],[64087,32244],[64088,32265],[64089,32321],[64090,32626],[64091,32773],[64092,33261],[64093,33401],[64094,33401],[64095,33879],[64096,35088],[64097,35222],[64098,35585],[64099,35641],[64100,36051],[64101,36104],[64102,36790],[64103,36920],[64104,38627],[64105,38911],[64106,38971],[64107,24693],[64108,148206],[64109,33304],[64112,20006],[64113,20917],[64114,20840],[64115,20352],[64116,20805],[64117,20864],[64118,21191],[64119,21242],[64120,21917],[64121,21845],[64122,21913],[64123,21986],[64124,22618],[64125,22707],[64126,22852],[64127,22868],[64128,23138],[64129,23336],[64130,24274],[64131,24281],[64132,24425],[64133,24493],[64134,24792],[64135,24910],[64136,24840],[64137,24974],[64138,24928],[64139,25074],[64140,25140],[64141,25540],[64142,25628],[64143,25682],[64144,25942],[64145,26228],[64146,26391],[64147,26395],[64148,26454],[64149,27513],[64150,27578],[64151,27969],[64152,28379],[64153,28363],[64154,28450],[64155,28702],[64156,29038],[64157,30631],[64158,29237],[64159,29359],[64160,29482],[64161,29809],[64162,29958],[64163,30011],[64164,30237],[64165,30239],[64166,30410],[64167,30427],[64168,30452],[64169,30538],[64170,30528],[64171,30924],[64172,31409],[64173,31680],[64174,31867],[64175,32091],[64176,32244],[64177,32574],[64178,32773],[64179,33618],[64180,33775],[64181,34681],[64182,35137],[64183,35206],[64184,35222],[64185,35519],[64186,35576],[64187,35531],[64188,35585],[64189,35582],[64190,35565],[64191,35641],[64192,35722],[64193,36104],[64194,36664],[64195,36978],[64196,37273],[64197,37494],[64198,38524],[64199,38627],[64200,38742],[64201,38875],[64202,38911],[64203,38923],[64204,38971],[64205,39698],[64206,40860],[64207,141386],[64208,141380],[64209,144341],[64210,15261],[64211,16408],[64212,16441],[64213,152137],[64214,154832],[64215,163539],[64216,40771],[64217,40846],[64285,1497,1460],[64287,1522,1463],[64298,1513,1473],[64299,1513,1474],[64300,1513,1468,1473],[64301,1513,1468,1474],[64302,1488,1463],[64303,1488,1464],[64304,1488,1468],[64305,1489,1468],[64306,1490,1468],[64307,1491,1468],[64308,1492,1468],[64309,1493,1468],[64310,1494,1468],[64312,1496,1468],[64313,1497,1468],[64314,1498,1468],[64315,1499,1468],[64316,1500,1468],[64318,1502,1468],[64320,1504,1468],[64321,1505,1468],[64323,1507,1468],[64324,1508,1468],[64326,1510,1468],[64327,1511,1468],[64328,1512,1468],[64329,1513,1468],[64330,1514,1468],[64331,1493,1465],[64332,1489,1471],[64333,1499,1471],[64334,1508,1471],[69786,69785,69818],[69788,69787,69818],[69803,69797,69818],[69934,69937,69927],[69935,69938,69927],[70475,70471,70462],[70476,70471,70487],[70843,70841,70842],[70844,70841,70832],[70846,70841,70845],[71098,71096,71087],[71099,71097,71087],[71992,71989,71984],[119134,119127,119141],[119135,119128,119141],[119136,119128,119141,119150],[119137,119128,119141,119151],[119138,119128,119141,119152],[119139,119128,119141,119153],[119140,119128,119141,119154],[119227,119225,119141],[119228,119226,119141],[119229,119225,119141,119150],[119230,119226,119141,119150],[119231,119225,119141,119151],[119232,119226,119141,119151],[194560,20029],[194561,20024],[194562,20033],[194563,131362],[194564,20320],[194565,20398],[194566,20411],[194567,20482],[194568,20602],[194569,20633],[194570,20711],[194571,20687],[194572,13470],[194573,132666],[194574,20813],[194575,20820],[194576,20836],[194577,20855],[194578,132380],[194579,13497],[194580,20839],[194581,20877],[194582,132427],[194583,20887],[194584,20900],[194585,20172],[194586,20908],[194587,20917],[194588,168415],[194589,20981],[194590,20995],[194591,13535],[194592,21051],[194593,21062],[194594,21106],[194595,21111],[194596,13589],[194597,21191],[194598,21193],[194599,21220],[194600,21242],[194601,21253],[194602,21254],[194603,21271],[194604,21321],[194605,21329],[194606,21338],[194607,21363],[194608,21373],[194609,21375],[194610,21375],[194611,21375],[194612,133676],[194613,28784],[194614,21450],[194615,21471],[194616,133987],[194617,21483],[194618,21489],[194619,21510],[194620,21662],[194621,21560],[194622,21576],[194623,21608],[194624,21666],[194625,21750],[194626,21776],[194627,21843],[194628,21859],[194629,21892],[194630,21892],[194631,21913],[194632,21931],[194633,21939],[194634,21954],[194635,22294],[194636,22022],[194637,22295],[194638,22097],[194639,22132],[194640,20999],[194641,22766],[194642,22478],[194643,22516],[194644,22541],[194645,22411],[194646,22578],[194647,22577],[194648,22700],[194649,136420],[194650,22770],[194651,22775],[194652,22790],[194653,22810],[194654,22818],[194655,22882],[194656,136872],[194657,136938],[194658,23020],[194659,23067],[194660,23079],[194661,23000],[194662,23142],[194663,14062],[194664,14076],[194665,23304],[194666,23358],[194667,23358],[194668,137672],[194669,23491],[194670,23512],[194671,23527],[194672,23539],[194673,138008],[194674,23551],[194675,23558],[194676,24403],[194677,23586],[194678,14209],[194679,23648],[194680,23662],[194681,23744],[194682,23693],[194683,138724],[194684,23875],[194685,138726],[194686,23918],[194687,23915],[194688,23932],[194689,24033],[194690,24034],[194691,14383],[194692,24061],[194693,24104],[194694,24125],[194695,24169],[194696,14434],[194697,139651],[194698,14460],[194699,24240],[194700,24243],[194701,24246],[194702,24266],[194703,172946],[194704,24318],[194705,140081],[194706,140081],[194707,33281],[194708,24354],[194709,24354],[194710,14535],[194711,144056],[194712,156122],[194713,24418],[194714,24427],[194715,14563],[194716,24474],[194717,24525],[194718,24535],[194719,24569],[194720,24705],[194721,14650],[194722,14620],[194723,24724],[194724,141012],[194725,24775],[194726,24904],[194727,24908],[194728,24910],[194729,24908],[194730,24954],[194731,24974],[194732,25010],[194733,24996],[194734,25007],[194735,25054],[194736,25074],[194737,25078],[194738,25104],[194739,25115],[194740,25181],[194741,25265],[194742,25300],[194743,25424],[194744,142092],[194745,25405],[194746,25340],[194747,25448],[194748,25475],[194749,25572],[194750,142321],[194751,25634],[194752,25541],[194753,25513],[194754,14894],[194755,25705],[194756,25726],[194757,25757],[194758,25719],[194759,14956],[194760,25935],[194761,25964],[194762,143370],[194763,26083],[194764,26360],[194765,26185],[194766,15129],[194767,26257],[194768,15112],[194769,15076],[194770,20882],[194771,20885],[194772,26368],[194773,26268],[194774,32941],[194775,17369],[194776,26391],[194777,26395],[194778,26401],[194779,26462],[194780,26451],[194781,144323],[194782,15177],[194783,26618],[194784,26501],[194785,26706],[194786,26757],[194787,144493],[194788,26766],[194789,26655],[194790,26900],[194791,15261],[194792,26946],[194793,27043],[194794,27114],[194795,27304],[194796,145059],[194797,27355],[194798,15384],[194799,27425],[194800,145575],[194801,27476],[194802,15438],[194803,27506],[194804,27551],[194805,27578],[194806,27579],[194807,146061],[194808,138507],[194809,146170],[194810,27726],[194811,146620],[194812,27839],[194813,27853],[194814,27751],[194815,27926],[194816,27966],[194817,28023],[194818,27969],[194819,28009],[194820,28024],[194821,28037],[194822,146718],[194823,27956],[194824,28207],[194825,28270],[194826,15667],[194827,28363],[194828,28359],[194829,147153],[194830,28153],[194831,28526],[194832,147294],[194833,147342],[194834,28614],[194835,28729],[194836,28702],[194837,28699],[194838,15766],[194839,28746],[194840,28797],[194841,28791],[194842,28845],[194843,132389],[194844,28997],[194845,148067],[194846,29084],[194847,148395],[194848,29224],[194849,29237],[194850,29264],[194851,149000],[194852,29312],[194853,29333],[194854,149301],[194855,149524],[194856,29562],[194857,29579],[194858,16044],[194859,29605],[194860,16056],[194861,16056],[194862,29767],[194863,29788],[194864,29809],[194865,29829],[194866,29898],[194867,16155],[194868,29988],[194869,150582],[194870,30014],[194871,150674],[194872,30064],[194873,139679],[194874,30224],[194875,151457],[194876,151480],[194877,151620],[194878,16380],[194879,16392],[194880,30452],[194881,151795],[194882,151794],[194883,151833],[194884,151859],[194885,30494],[194886,30495],[194887,30495],[194888,30538],[194889,16441],[194890,30603],[194891,16454],[194892,16534],[194893,152605],[194894,30798],[194895,30860],[194896,30924],[194897,16611],[194898,153126],[194899,31062],[194900,153242],[194901,153285],[194902,31119],[194903,31211],[194904,16687],[194905,31296],[194906,31306],[194907,31311],[194908,153980],[194909,154279],[194910,154279],[194911,31470],[194912,16898],[194913,154539],[194914,31686],[194915,31689],[194916,16935],[194917,154752],[194918,31954],[194919,17056],[194920,31976],[194921,31971],[194922,32000],[194923,155526],[194924,32099],[194925,17153],[194926,32199],[194927,32258],[194928,32325],[194929,17204],[194930,156200],[194931,156231],[194932,17241],[194933,156377],[194934,32634],[194935,156478],[194936,32661],[194937,32762],[194938,32773],[194939,156890],[194940,156963],[194941,32864],[194942,157096],[194943,32880],[194944,144223],[194945,17365],[194946,32946],[194947,33027],[194948,17419],[194949,33086],[194950,23221],[194951,157607],[194952,157621],[194953,144275],[194954,144284],[194955,33281],[194956,33284],[194957,36766],[194958,17515],[194959,33425],[194960,33419],[194961,33437],[194962,21171],[194963,33457],[194964,33459],[194965,33469],[194966,33510],[194967,158524],[194968,33509],[194969,33565],[194970,33635],[194971,33709],[194972,33571],[194973,33725],[194974,33767],[194975,33879],[194976,33619],[194977,33738],[194978,33740],[194979,33756],[194980,158774],[194981,159083],[194982,158933],[194983,17707],[194984,34033],[194985,34035],[194986,34070],[194987,160714],[194988,34148],[194989,159532],[194990,17757],[194991,17761],[194992,159665],[194993,159954],[194994,17771],[194995,34384],[194996,34396],[194997,34407],[194998,34409],[194999,34473],[195000,34440],[195001,34574],[195002,34530],[195003,34681],[195004,34600],[195005,34667],[195006,34694],[195007,17879],[195008,34785],[195009,34817],[195010,17913],[195011,34912],[195012,34915],[195013,161383],[195014,35031],[195015,35038],[195016,17973],[195017,35066],[195018,13499],[195019,161966],[195020,162150],[195021,18110],[195022,18119],[195023,35488],[195024,35565],[195025,35722],[195026,35925],[195027,162984],[195028,36011],[195029,36033],[195030,36123],[195031,36215],[195032,163631],[195033,133124],[195034,36299],[195035,36284],[195036,36336],[195037,133342],[195038,36564],[195039,36664],[195040,165330],[195041,165357],[195042,37012],[195043,37105],[195044,37137],[195045,165678],[195046,37147],[195047,37432],[195048,37591],[195049,37592],[195050,37500],[195051,37881],[195052,37909],[195053,166906],[195054,38283],[195055,18837],[195056,38327],[195057,167287],[195058,18918],[195059,38595],[195060,23986],[195061,38691],[195062,168261],[195063,168474],[195064,19054],[195065,19062],[195066,38880],[195067,168970],[195068,19122],[195069,169110],[195070,38923],[195071,38923],[195072,38953],[195073,169398],[195074,39138],[195075,19251],[195076,39209],[195077,39335],[195078,39362],[195079,39422],[195080,19406],[195081,170800],[195082,39698],[195083,40000],[195084,40189],[195085,19662],[195086,19693],[195087,40295],[195088,172238],[195089,19704],[195090,172293],[195091,172558],[195092,172689],[195093,40635],[195094,19798],[195095,40697],[195096,40702],[195097,40709],[195098,40719],[195099,40726],[195100,40763],[195101,173568]],"composites":[192,193,194,195,196,197,199,200,201,202,203,204,205,206,207,209,210,211,212,213,214,217,218,219,220,221,224,225,226,227,228,229,231,232,233,234,235,236,237,238,239,241,242,243,244,245,246,249,250,251,252,253,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,296,297,298,299,300,301,302,303,304,308,309,310,311,313,314,315,316,317,318,323,324,325,326,327,328,332,333,334,335,336,337,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,416,417,431,432,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,478,479,480,481,482,483,486,487,488,489,490,491,492,493,494,495,496,500,501,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,542,543,550,551,552,553,554,555,556,557,558,559,560,561,562,563,901,902,904,905,906,908,910,911,912,938,939,940,941,942,943,944,970,971,972,973,974,979,980,1024,1025,1027,1031,1036,1037,1038,1049,1081,1104,1105,1107,1111,1116,1117,1118,1142,1143,1217,1218,1232,1233,1234,1235,1238,1239,1242,1243,1244,1245,1246,1247,1250,1251,1252,1253,1254,1255,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1272,1273,1570,1571,1572,1573,1574,1728,1730,1747,2345,2353,2356,2507,2508,2888,2891,2892,2964,3018,3019,3020,3144,3264,3271,3272,3274,3275,3402,3403,3404,3546,3548,3549,3550,4134,6918,6920,6922,6924,6926,6930,6971,6973,6976,6977,6979,7680,7681,7682,7683,7684,7685,7686,7687,7688,7689,7690,7691,7692,7693,7694,7695,7696,7697,7698,7699,7700,7701,7702,7703,7704,7705,7706,7707,7708,7709,7710,7711,7712,7713,7714,7715,7716,7717,7718,7719,7720,7721,7722,7723,7724,7725,7726,7727,7728,7729,7730,7731,7732,7733,7734,7735,7736,7737,7738,7739,7740,7741,7742,7743,7744,7745,7746,7747,7748,7749,7750,7751,7752,7753,7754,7755,7756,7757,7758,7759,7760,7761,7762,7763,7764,7765,7766,7767,7768,7769,7770,7771,7772,7773,7774,7775,7776,7777,7778,7779,7780,7781,7782,7783,7784,7785,7786,7787,7788,7789,7790,7791,7792,7793,7794,7795,7796,7797,7798,7799,7800,7801,7802,7803,7804,7805,7806,7807,7808,7809,7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823,7824,7825,7826,7827,7828,7829,7830,7831,7832,7833,7835,7840,7841,7842,7843,7844,7845,7846,7847,7848,7849,7850,7851,7852,7853,7854,7855,7856,7857,7858,7859,7860,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870,7871,7872,7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886,7887,7888,7889,7890,7891,7892,7893,7894,7895,7896,7897,7898,7899,7900,7901,7902,7903,7904,7905,7906,7907,7908,7909,7910,7911,7912,7913,7914,7915,7916,7917,7918,7919,7920,7921,7922,7923,7924,7925,7926,7927,7928,7929,7936,7937,7938,7939,7940,7941,7942,7943,7944,7945,7946,7947,7948,7949,7950,7951,7952,7953,7954,7955,7956,7957,7960,7961,7962,7963,7964,7965,7968,7969,7970,7971,7972,7973,7974,7975,7976,7977,7978,7979,7980,7981,7982,7983,7984,7985,7986,7987,7988,7989,7990,7991,7992,7993,7994,7995,7996,7997,7998,7999,8000,8001,8002,8003,8004,8005,8008,8009,8010,8011,8012,8013,8016,8017,8018,8019,8020,8021,8022,8023,8025,8027,8029,8031,8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047,8048,8050,8052,8054,8056,8058,8060,8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111,8112,8113,8114,8115,8116,8118,8119,8120,8121,8122,8124,8129,8130,8131,8132,8134,8135,8136,8138,8140,8141,8142,8143,8144,8145,8146,8150,8151,8152,8153,8154,8157,8158,8159,8160,8161,8162,8164,8165,8166,8167,8168,8169,8170,8172,8173,8178,8179,8180,8182,8183,8184,8186,8188,8602,8603,8622,8653,8654,8655,8708,8713,8716,8740,8742,8769,8772,8775,8777,8800,8802,8813,8814,8815,8816,8817,8820,8821,8824,8825,8832,8833,8836,8837,8840,8841,8876,8877,8878,8879,8928,8929,8930,8931,8938,8939,8940,8941,12364,12366,12368,12370,12372,12374,12376,12378,12380,12382,12384,12386,12389,12391,12393,12400,12401,12403,12404,12406,12407,12409,12410,12412,12413,12436,12446,12460,12462,12464,12466,12468,12470,12472,12474,12476,12478,12480,12482,12485,12487,12489,12496,12497,12499,12500,12502,12503,12505,12506,12508,12509,12532,12535,12536,12537,12538,12542,69786,69788,69803,69934,69935,70475,70476,70843,70844,70846,71098,71099,71992],"streamSafe":[[168,0,1],[175,0,1],[180,0,1],[184,0,1],[192,0,1],[193,0,1],[194,0,1],[195,0,1],[196,0,1],[197,0,1],[199,0,1],[200,0,1],[201,0,1],[202,0,1],[203,0,1],[204,0,1],[205,0,1],[206,0,1],[207,0,1],[209,0,1],[210,0,1],[211,0,1],[212,0,1],[213,0,1],[214,0,1],[217,0,1],[218,0,1],[219,0,1],[220,0,1],[221,0,1],[224,0,1],[225,0,1],[226,0,1],[227,0,1],[228,0,1],[229,0,1],[231,0,1],[232,0,1],[233,0,1],[234,0,1],[235,0,1],[236,0,1],[237,0,1],[238,0,1],[239,0,1],[241,0,1],[242,0,1],[243,0,1],[244,0,1],[245,0,1],[246,0,1],[249,0,1],[250,0,1],[251,0,1],[252,0,1],[253,0,1],[255,0,1],[256,0,1],[257,0,1],[258,0,1],[259,0,1],[260,0,1],[261,0,1],[262,0,1],[263,0,1],[264,0,1],[265,0,1],[266,0,1],[267,0,1],[268,0,1],[269,0,1],[270,0,1],[271,0,1],[274,0,1],[275,0,1],[276,0,1],[277,0,1],[278,0,1],[279,0,1],[280,0,1],[281,0,1],[282,0,1],[283,0,1],[284,0,1],[285,0,1],[286,0,1],[287,0,1],[288,0,1],[289,0,1],[290,0,1],[291,0,1],[292,0,1],[293,0,1],[296,0,1],[297,0,1],[298,0,1],[299,0,1],[300,0,1],[301,0,1],[302,0,1],[303,0,1],[304,0,1],[308,0,1],[309,0,1],[310,0,1],[311,0,1],[313,0,1],[314,0,1],[315,0,1],[316,0,1],[317,0,1],[318,0,1],[323,0,1],[324,0,1],[325,0,1],[326,0,1],[327,0,1],[328,0,1],[332,0,1],[333,0,1],[334,0,1],[335,0,1],[336,0,1],[337,0,1],[340,0,1],[341,0,1],[342,0,1],[343,0,1],[344,0,1],[345,0,1],[346,0,1],[347,0,1],[348,0,1],[349,0,1],[350,0,1],[351,0,1],[352,0,1],[353,0,1],[354,0,1],[355,0,1],[356,0,1],[357,0,1],[360,0,1],[361,0,1],[362,0,1],[363,0,1],[364,0,1],[365,0,1],[366,0,1],[367,0,1],[368,0,1],[369,0,1],[370,0,1],[371,0,1],[372,0,1],[373,0,1],[374,0,1],[375,0,1],[376,0,1],[377,0,1],[378,0,1],[379,0,1],[380,0,1],[381,0,1],[382,0,1],[416,0,1],[417,0,1],[431,0,1],[432,0,1],[452,0,1],[453,0,1],[454,0,1],[461,0,1],[462,0,1],[463,0,1],[464,0,1],[465,0,1],[466,0,1],[467,0,1],[468,0,1],[469,0,2],[470,0,2],[471,0,2],[472,0,2],[473,0,2],[474,0,2],[475,0,2],[476,0,2],[478,0,2],[479,0,2],[480,0,2],[481,0,2],[482,0,1],[483,0,1],[486,0,1],[487,0,1],[488,0,1],[489,0,1],[490,0,1],[491,0,1],[492,0,2],[493,0,2],[494,0,1],[495,0,1],[496,0,1],[500,0,1],[501,0,1],[504,0,1],[505,0,1],[506,0,2],[507,0,2],[508,0,1],[509,0,1],[510,0,1],[511,0,1],[512,0,1],[513,0,1],[514,0,1],[515,0,1],[516,0,1],[517,0,1],[518,0,1],[519,0,1],[520,0,1],[521,0,1],[522,0,1],[523,0,1],[524,0,1],[525,0,1],[526,0,1],[527,0,1],[528,0,1],[529,0,1],[530,0,1],[531,0,1],[532,0,1],[533,0,1],[534,0,1],[535,0,1],[536,0,1],[537,0,1],[538,0,1],[539,0,1],[542,0,1],[543,0,1],[550,0,1],[551,0,1],[552,0,1],[553,0,1],[554,0,2],[555,0,2],[556,0,2],[557,0,2],[558,0,1],[559,0,1],[560,0,2],[561,0,2],[562,0,1],[563,0,1],[728,0,1],[729,0,1],[730,0,1],[731,0,1],[732,0,1],[733,0,1],[768,1,1],[769,1,1],[770,1,1],[771,1,1],[772,1,1],[773,1,1],[774,1,1],[775,1,1],[776,1,1],[777,1,1],[778,1,1],[779,1,1],[780,1,1],[781,1,1],[782,1,1],[783,1,1],[784,1,1],[785,1,1],[786,1,1],[787,1,1],[788,1,1],[789,1,1],[790,1,1],[791,1,1],[792,1,1],[793,1,1],[794,1,1],[795,1,1],[796,1,1],[797,1,1],[798,1,1],[799,1,1],[800,1,1],[801,1,1],[802,1,1],[803,1,1],[804,1,1],[805,1,1],[806,1,1],[807,1,1],[808,1,1],[809,1,1],[810,1,1],[811,1,1],[812,1,1],[813,1,1],[814,1,1],[815,1,1],[816,1,1],[817,1,1],[818,1,1],[819,1,1],[820,1,1],[821,1,1],[822,1,1],[823,1,1],[824,1,1],[825,1,1],[826,1,1],[827,1,1],[828,1,1],[829,1,1],[830,1,1],[831,1,1],[832,1,1],[833,1,1],[834,1,1],[835,1,1],[836,2,2],[837,1,1],[838,1,1],[839,1,1],[840,1,1],[841,1,1],[842,1,1],[843,1,1],[844,1,1],[845,1,1],[846,1,1],[848,1,1],[849,1,1],[850,1,1],[851,1,1],[852,1,1],[853,1,1],[854,1,1],[855,1,1],[856,1,1],[857,1,1],[858,1,1],[859,1,1],[860,1,1],[861,1,1],[862,1,1],[863,1,1],[864,1,1],[865,1,1],[866,1,1],[867,1,1],[868,1,1],[869,1,1],[870,1,1],[871,1,1],[872,1,1],[873,1,1],[874,1,1],[875,1,1],[876,1,1],[877,1,1],[878,1,1],[879,1,1],[890,0,1],[900,0,1],[901,0,2],[902,0,1],[904,0,1],[905,0,1],[906,0,1],[908,0,1],[910,0,1],[911,0,1],[912,0,2],[938,0,1],[939,0,1],[940,0,1],[941,0,1],[942,0,1],[943,0,1],[944,0,2],[970,0,1],[971,0,1],[972,0,1],[973,0,1],[974,0,1],[979,0,1],[980,0,1],[1024,0,1],[1025,0,1],[1027,0,1],[1031,0,1],[1036,0,1],[1037,0,1],[1038,0,1],[1049,0,1],[1081,0,1],[1104,0,1],[1105,0,1],[1107,0,1],[1111,0,1],[1116,0,1],[1117,0,1],[1118,0,1],[1142,0,1],[1143,0,1],[1155,1,1],[1156,1,1],[1157,1,1],[1158,1,1],[1159,1,1],[1217,0,1],[1218,0,1],[1232,0,1],[1233,0,1],[1234,0,1],[1235,0,1],[1238,0,1],[1239,0,1],[1242,0,1],[1243,0,1],[1244,0,1],[1245,0,1],[1246,0,1],[1247,0,1],[1250,0,1],[1251,0,1],[1252,0,1],[1253,0,1],[1254,0,1],[1255,0,1],[1258,0,1],[1259,0,1],[1260,0,1],[1261,0,1],[1262,0,1],[1263,0,1],[1264,0,1],[1265,0,1],[1266,0,1],[1267,0,1],[1268,0,1],[1269,0,1],[1272,0,1],[1273,0,1],[1425,1,1],[1426,1,1],[1427,1,1],[1428,1,1],[1429,1,1],[1430,1,1],[1431,1,1],[1432,1,1],[1433,1,1],[1434,1,1],[1435,1,1],[1436,1,1],[1437,1,1],[1438,1,1],[1439,1,1],[1440,1,1],[1441,1,1],[1442,1,1],[1443,1,1],[1444,1,1],[1445,1,1],[1446,1,1],[1447,1,1],[1448,1,1],[1449,1,1],[1450,1,1],[1451,1,1],[1452,1,1],[1453,1,1],[1454,1,1],[1455,1,1],[1456,1,1],[1457,1,1],[1458,1,1],[1459,1,1],[1460,1,1],[1461,1,1],[1462,1,1],[1463,1,1],[1464,1,1],[1465,1,1],[1466,1,1],[1467,1,1],[1468,1,1],[1469,1,1],[1471,1,1],[1473,1,1],[1474,1,1],[1476,1,1],[1477,1,1],[1479,1,1],[1552,1,1],[1553,1,1],[1554,1,1],[1555,1,1],[1556,1,1],[1557,1,1],[1558,1,1],[1559,1,1],[1560,1,1],[1561,1,1],[1562,1,1],[1570,0,1],[1571,0,1],[1572,0,1],[1573,0,1],[1574,0,1],[1611,1,1],[1612,1,1],[1613,1,1],[1614,1,1],[1615,1,1],[1616,1,1],[1617,1,1],[1618,1,1],[1619,1,1],[1620,1,1],[1621,1,1],[1622,1,1],[1623,1,1],[1624,1,1],[1625,1,1],[1626,1,1],[1627,1,1],[1628,1,1],[1629,1,1],[1630,1,1],[1631,1,1],[1648,1,1],[1728,0,1],[1730,0,1],[1747,0,1],[1750,1,1],[1751,1,1],[1752,1,1],[1753,1,1],[1754,1,1],[1755,1,1],[1756,1,1],[1759,1,1],[1760,1,1],[1761,1,1],[1762,1,1],[1763,1,1],[1764,1,1],[1767,1,1],[1768,1,1],[1770,1,1],[1771,1,1],[1772,1,1],[1773,1,1],[1809,1,1],[1840,1,1],[1841,1,1],[1842,1,1],[1843,1,1],[1844,1,1],[1845,1,1],[1846,1,1],[1847,1,1],[1848,1,1],[1849,1,1],[1850,1,1],[1851,1,1],[1852,1,1],[1853,1,1],[1854,1,1],[1855,1,1],[1856,1,1],[1857,1,1],[1858,1,1],[1859,1,1],[1860,1,1],[1861,1,1],[1862,1,1],[1863,1,1],[1864,1,1],[1865,1,1],[1866,1,1],[2027,1,1],[2028,1,1],[2029,1,1],[2030,1,1],[2031,1,1],[2032,1,1],[2033,1,1],[2034,1,1],[2035,1,1],[2045,1,1],[2070,1,1],[2071,1,1],[2072,1,1],[2073,1,1],[2075,1,1],[2076,1,1],[2077,1,1],[2078,1,1],[2079,1,1],[2080,1,1],[2081,1,1],[2082,1,1],[2083,1,1],[2085,1,1],[2086,1,1],[2087,1,1],[2089,1,1],[2090,1,1],[2091,1,1],[2092,1,1],[2093,1,1],[2137,1,1],[2138,1,1],[2139,1,1],[2200,1,1],[2201,1,1],[2202,1,1],[2203,1,1],[2204,1,1],[2205,1,1],[2206,1,1],[2207,1,1],[2250,1,1],[2251,1,1],[2252,1,1],[2253,1,1],[2254,1,1],[2255,1,1],[2256,1,1],[2257,1,1],[2258,1,1],[2259,1,1],[2260,1,1],[2261,1,1],[2262,1,1],[2263,1,1],[2264,1,1],[2265,1,1],[2266,1,1],[2267,1,1],[2268,1,1],[2269,1,1],[2270,1,1],[2271,1,1],[2272,1,1],[2273,1,1],[2275,1,1],[2276,1,1],[2277,1,1],[2278,1,1],[2279,1,1],[2280,1,1],[2281,1,1],[2282,1,1],[2283,1,1],[2284,1,1],[2285,1,1],[2286,1,1],[2287,1,1],[2288,1,1],[2289,1,1],[2290,1,1],[2291,1,1],[2292,1,1],[2293,1,1],[2294,1,1],[2295,1,1],[2296,1,1],[2297,1,1],[2298,1,1],[2299,1,1],[2300,1,1],[2301,1,1],[2302,1,1],[2303,1,1],[2345,0,1],[2353,0,1],[2356,0,1],[2364,1,1],[2381,1,1],[2385,1,1],[2386,1,1],[2387,1,1],[2388,1,1],[2392,0,1],[2393,0,1],[2394,0,1],[2395,0,1],[2396,0,1],[2397,0,1],[2398,0,1],[2399,0,1],[2492,1,1],[2494,1,1],[2507,0,1],[2508,0,1],[2509,1,1],[2519,1,1],[2524,0,1],[2525,0,1],[2527,0,1],[2558,1,1],[2611,0,1],[2614,0,1],[2620,1,1],[2637,1,1],[2649,0,1],[2650,0,1],[2651,0,1],[2654,0,1],[2748,1,1],[2765,1,1],[2876,1,1],[2878,1,1],[2888,0,1],[2891,0,1],[2892,0,1],[2893,1,1],[2902,1,1],[2903,1,1],[2908,0,1],[2909,0,1],[2964,0,1],[3006,1,1],[3018,0,1],[3019,0,1],[3020,0,1],[3021,1,1],[3031,1,1],[3132,1,1],[3144,0,1],[3149,1,1],[3157,1,1],[3158,1,1],[3260,1,1],[3264,0,1],[3266,1,1],[3271,0,1],[3272,0,1],[3274,0,1],[3275,0,2],[3277,1,1],[3285,1,1],[3286,1,1],[3387,1,1],[3388,1,1],[3390,1,1],[3402,0,1],[3403,0,1],[3404,0,1],[3405,1,1],[3415,1,1],[3530,1,1],[3535,1,1],[3546,0,1],[3548,0,1],[3549,0,2],[3550,0,1],[3551,1,1],[3640,1,1],[3641,1,1],[3642,1,1],[3656,1,1],[3657,1,1],[3658,1,1],[3659,1,1],[3768,1,1],[3769,1,1],[3770,1,1],[3784,1,1],[3785,1,1],[3786,1,1],[3787,1,1],[3864,1,1],[3865,1,1],[3893,1,1],[3895,1,1],[3897,1,1],[3953,1,1],[3954,1,1],[3955,2,2],[3956,1,1],[3957,2,2],[3958,0,1],[3959,0,2],[3960,0,1],[3961,0,2],[3962,1,1],[3963,1,1],[3964,1,1],[3965,1,1],[3968,1,1],[3969,2,2],[3970,1,1],[3971,1,1],[3972,1,1],[3974,1,1],[3975,1,1],[4038,1,1],[4134,0,1],[4142,1,1],[4151,1,1],[4153,1,1],[4154,1,1],[4237,1,1],[4449,1,1],[4450,1,1],[4451,1,1],[4452,1,1],[4453,1,1],[4454,1,1],[4455,1,1],[4456,1,1],[4457,1,1],[4458,1,1],[4459,1,1],[4460,1,1],[4461,1,1],[4462,1,1],[4463,1,1],[4464,1,1],[4465,1,1],[4466,1,1],[4467,1,1],[4468,1,1],[4469,1,1],[4520,1,1],[4521,1,1],[4522,1,1],[4523,1,1],[4524,1,1],[4525,1,1],[4526,1,1],[4527,1,1],[4528,1,1],[4529,1,1],[4530,1,1],[4531,1,1],[4532,1,1],[4533,1,1],[4534,1,1],[4535,1,1],[4536,1,1],[4537,1,1],[4538,1,1],[4539,1,1],[4540,1,1],[4541,1,1],[4542,1,1],[4543,1,1],[4544,1,1],[4545,1,1],[4546,1,1],[4957,1,1],[4958,1,1],[4959,1,1],[5908,1,1],[5909,1,1],[5940,1,1],[6098,1,1],[6109,1,1],[6313,1,1],[6457,1,1],[6458,1,1],[6459,1,1],[6679,1,1],[6680,1,1],[6752,1,1],[6773,1,1],[6774,1,1],[6775,1,1],[6776,1,1],[6777,1,1],[6778,1,1],[6779,1,1],[6780,1,1],[6783,1,1],[6832,1,1],[6833,1,1],[6834,1,1],[6835,1,1],[6836,1,1],[6837,1,1],[6838,1,1],[6839,1,1],[6840,1,1],[6841,1,1],[6842,1,1],[6843,1,1],[6844,1,1],[6845,1,1],[6847,1,1],[6848,1,1],[6849,1,1],[6850,1,1],[6851,1,1],[6852,1,1],[6853,1,1],[6854,1,1],[6855,1,1],[6856,1,1],[6857,1,1],[6858,1,1],[6859,1,1],[6860,1,1],[6861,1,1],[6862,1,1],[6918,0,1],[6920,0,1],[6922,0,1],[6924,0,1],[6926,0,1],[6930,0,1],[6964,1,1],[6965,1,1],[6971,0,1],[6973,0,1],[6976,0,1],[6977,0,1],[6979,0,1],[6980,1,1],[7019,1,1],[7020,1,1],[7021,1,1],[7022,1,1],[7023,1,1],[7024,1,1],[7025,1,1],[7026,1,1],[7027,1,1],[7082,1,1],[7083,1,1],[7142,1,1],[7154,1,1],[7155,1,1],[7223,1,1],[7376,1,1],[7377,1,1],[7378,1,1],[7380,1,1],[7381,1,1],[7382,1,1],[7383,1,1],[7384,1,1],[7385,1,1],[7386,1,1],[7387,1,1],[7388,1,1],[7389,1,1],[7390,1,1],[7391,1,1],[7392,1,1],[7394,1,1],[7395,1,1],[7396,1,1],[7397,1,1],[7398,1,1],[7399,1,1],[7400,1,1],[7405,1,1],[7412,1,1],[7416,1,1],[7417,1,1],[7616,1,1],[7617,1,1],[7618,1,1],[7619,1,1],[7620,1,1],[7621,1,1],[7622,1,1],[7623,1,1],[7624,1,1],[7625,1,1],[7626,1,1],[7627,1,1],[7628,1,1],[7629,1,1],[7630,1,1],[7631,1,1],[7632,1,1],[7633,1,1],[7634,1,1],[7635,1,1],[7636,1,1],[7637,1,1],[7638,1,1],[7639,1,1],[7640,1,1],[7641,1,1],[7642,1,1],[7643,1,1],[7644,1,1],[7645,1,1],[7646,1,1],[7647,1,1],[7648,1,1],[7649,1,1],[7650,1,1],[7651,1,1],[7652,1,1],[7653,1,1],[7654,1,1],[7655,1,1],[7656,1,1],[7657,1,1],[7658,1,1],[7659,1,1],[7660,1,1],[7661,1,1],[7662,1,1],[7663,1,1],[7664,1,1],[7665,1,1],[7666,1,1],[7667,1,1],[7668,1,1],[7669,1,1],[7670,1,1],[7671,1,1],[7672,1,1],[7673,1,1],[7674,1,1],[7675,1,1],[7676,1,1],[7677,1,1],[7678,1,1],[7679,1,1],[7680,0,1],[7681,0,1],[7682,0,1],[7683,0,1],[7684,0,1],[7685,0,1],[7686,0,1],[7687,0,1],[7688,0,2],[7689,0,2],[7690,0,1],[7691,0,1],[7692,0,1],[7693,0,1],[7694,0,1],[7695,0,1],[7696,0,1],[7697,0,1],[7698,0,1],[7699,0,1],[7700,0,2],[7701,0,2],[7702,0,2],[7703,0,2],[7704,0,1],[7705,0,1],[7706,0,1],[7707,0,1],[7708,0,2],[7709,0,2],[7710,0,1],[7711,0,1],[7712,0,1],[7713,0,1],[7714,0,1],[7715,0,1],[7716,0,1],[7717,0,1],[7718,0,1],[7719,0,1],[7720,0,1],[7721,0,1],[7722,0,1],[7723,0,1],[7724,0,1],[7725,0,1],[7726,0,2],[7727,0,2],[7728,0,1],[7729,0,1],[7730,0,1],[7731,0,1],[7732,0,1],[7733,0,1],[7734,0,1],[7735,0,1],[7736,0,2],[7737,0,2],[7738,0,1],[7739,0,1],[7740,0,1],[7741,0,1],[7742,0,1],[7743,0,1],[7744,0,1],[7745,0,1],[7746,0,1],[7747,0,1],[7748,0,1],[7749,0,1],[7750,0,1],[7751,0,1],[7752,0,1],[7753,0,1],[7754,0,1],[7755,0,1],[7756,0,2],[7757,0,2],[7758,0,2],[7759,0,2],[7760,0,2],[7761,0,2],[7762,0,2],[7763,0,2],[7764,0,1],[7765,0,1],[7766,0,1],[7767,0,1],[7768,0,1],[7769,0,1],[7770,0,1],[7771,0,1],[7772,0,2],[7773,0,2],[7774,0,1],[7775,0,1],[7776,0,1],[7777,0,1],[7778,0,1],[7779,0,1],[7780,0,2],[7781,0,2],[7782,0,2],[7783,0,2],[7784,0,2],[7785,0,2],[7786,0,1],[7787,0,1],[7788,0,1],[7789,0,1],[7790,0,1],[7791,0,1],[7792,0,1],[7793,0,1],[7794,0,1],[7795,0,1],[7796,0,1],[7797,0,1],[7798,0,1],[7799,0,1],[7800,0,2],[7801,0,2],[7802,0,2],[7803,0,2],[7804,0,1],[7805,0,1],[7806,0,1],[7807,0,1],[7808,0,1],[7809,0,1],[7810,0,1],[7811,0,1],[7812,0,1],[7813,0,1],[7814,0,1],[7815,0,1],[7816,0,1],[7817,0,1],[7818,0,1],[7819,0,1],[7820,0,1],[7821,0,1],[7822,0,1],[7823,0,1],[7824,0,1],[7825,0,1],[7826,0,1],[7827,0,1],[7828,0,1],[7829,0,1],[7830,0,1],[7831,0,1],[7832,0,1],[7833,0,1],[7835,0,1],[7840,0,1],[7841,0,1],[7842,0,1],[7843,0,1],[7844,0,2],[7845,0,2],[7846,0,2],[7847,0,2],[7848,0,2],[7849,0,2],[7850,0,2],[7851,0,2],[7852,0,2],[7853,0,2],[7854,0,2],[7855,0,2],[7856,0,2],[7857,0,2],[7858,0,2],[7859,0,2],[7860,0,2],[7861,0,2],[7862,0,2],[7863,0,2],[7864,0,1],[7865,0,1],[7866,0,1],[7867,0,1],[7868,0,1],[7869,0,1],[7870,0,2],[7871,0,2],[7872,0,2],[7873,0,2],[7874,0,2],[7875,0,2],[7876,0,2],[7877,0,2],[7878,0,2],[7879,0,2],[7880,0,1],[7881,0,1],[7882,0,1],[7883,0,1],[7884,0,1],[7885,0,1],[7886,0,1],[7887,0,1],[7888,0,2],[7889,0,2],[7890,0,2],[7891,0,2],[7892,0,2],[7893,0,2],[7894,0,2],[7895,0,2],[7896,0,2],[7897,0,2],[7898,0,2],[7899,0,2],[7900,0,2],[7901,0,2],[7902,0,2],[7903,0,2],[7904,0,2],[7905,0,2],[7906,0,2],[7907,0,2],[7908,0,1],[7909,0,1],[7910,0,1],[7911,0,1],[7912,0,2],[7913,0,2],[7914,0,2],[7915,0,2],[7916,0,2],[7917,0,2],[7918,0,2],[7919,0,2],[7920,0,2],[7921,0,2],[7922,0,1],[7923,0,1],[7924,0,1],[7925,0,1],[7926,0,1],[7927,0,1],[7928,0,1],[7929,0,1],[7936,0,1],[7937,0,1],[7938,0,2],[7939,0,2],[7940,0,2],[7941,0,2],[7942,0,2],[7943,0,2],[7944,0,1],[7945,0,1],[7946,0,2],[7947,0,2],[7948,0,2],[7949,0,2],[7950,0,2],[7951,0,2],[7952,0,1],[7953,0,1],[7954,0,2],[7955,0,2],[7956,0,2],[7957,0,2],[7960,0,1],[7961,0,1],[7962,0,2],[7963,0,2],[7964,0,2],[7965,0,2],[7968,0,1],[7969,0,1],[7970,0,2],[7971,0,2],[7972,0,2],[7973,0,2],[7974,0,2],[7975,0,2],[7976,0,1],[7977,0,1],[7978,0,2],[7979,0,2],[7980,0,2],[7981,0,2],[7982,0,2],[7983,0,2],[7984,0,1],[7985,0,1],[7986,0,2],[7987,0,2],[7988,0,2],[7989,0,2],[7990,0,2],[7991,0,2],[7992,0,1],[7993,0,1],[7994,0,2],[7995,0,2],[7996,0,2],[7997,0,2],[7998,0,2],[7999,0,2],[8000,0,1],[8001,0,1],[8002,0,2],[8003,0,2],[8004,0,2],[8005,0,2],[8008,0,1],[8009,0,1],[8010,0,2],[8011,0,2],[8012,0,2],[8013,0,2],[8016,0,1],[8017,0,1],[8018,0,2],[8019,0,2],[8020,0,2],[8021,0,2],[8022,0,2],[8023,0,2],[8025,0,1],[8027,0,2],[8029,0,2],[8031,0,2],[8032,0,1],[8033,0,1],[8034,0,2],[8035,0,2],[8036,0,2],[8037,0,2],[8038,0,2],[8039,0,2],[8040,0,1],[8041,0,1],[8042,0,2],[8043,0,2],[8044,0,2],[8045,0,2],[8046,0,2],[8047,0,2],[8048,0,1],[8049,0,1],[8050,0,1],[8051,0,1],[8052,0,1],[8053,0,1],[8054,0,1],[8055,0,1],[8056,0,1],[8057,0,1],[8058,0,1],[8059,0,1],[8060,0,1],[8061,0,1],[8064,0,2],[8065,0,2],[8066,0,3],[8067,0,3],[8068,0,3],[8069,0,3],[8070,0,3],[8071,0,3],[8072,0,2],[8073,0,2],[8074,0,3],[8075,0,3],[8076,0,3],[8077,0,3],[8078,0,3],[8079,0,3],[8080,0,2],[8081,0,2],[8082,0,3],[8083,0,3],[8084,0,3],[8085,0,3],[8086,0,3],[8087,0,3],[8088,0,2],[8089,0,2],[8090,0,3],[8091,0,3],[8092,0,3],[8093,0,3],[8094,0,3],[8095,0,3],[8096,0,2],[8097,0,2],[8098,0,3],[8099,0,3],[8100,0,3],[8101,0,3],[8102,0,3],[8103,0,3],[8104,0,2],[8105,0,2],[8106,0,3],[8107,0,3],[8108,0,3],[8109,0,3],[8110,0,3],[8111,0,3],[8112,0,1],[8113,0,1],[8114,0,2],[8115,0,1],[8116,0,2],[8118,0,1],[8119,0,2],[8120,0,1],[8121,0,1],[8122,0,1],[8123,0,1],[8124,0,1],[8125,0,1],[8127,0,1],[8128,0,1],[8129,0,2],[8130,0,2],[8131,0,1],[8132,0,2],[8134,0,1],[8135,0,2],[8136,0,1],[8137,0,1],[8138,0,1],[8139,0,1],[8140,0,1],[8141,0,2],[8142,0,2],[8143,0,2],[8144,0,1],[8145,0,1],[8146,0,2],[8147,0,2],[8150,0,1],[8151,0,2],[8152,0,1],[8153,0,1],[8154,0,1],[8155,0,1],[8157,0,2],[8158,0,2],[8159,0,2],[8160,0,1],[8161,0,1],[8162,0,2],[8163,0,2],[8164,0,1],[8165,0,1],[8166,0,1],[8167,0,2],[8168,0,1],[8169,0,1],[8170,0,1],[8171,0,1],[8172,0,1],[8173,0,2],[8174,0,2],[8178,0,2],[8179,0,1],[8180,0,2],[8182,0,1],[8183,0,2],[8184,0,1],[8185,0,1],[8186,0,1],[8187,0,1],[8188,0,1],[8189,0,1],[8190,0,1],[8215,0,1],[8254,0,1],[8400,1,1],[8401,1,1],[8402,1,1],[8403,1,1],[8404,1,1],[8405,1,1],[8406,1,1],[8407,1,1],[8408,1,1],[8409,1,1],[8410,1,1],[8411,1,1],[8412,1,1],[8417,1,1],[8421,1,1],[8422,1,1],[8423,1,1],[8424,1,1],[8425,1,1],[8426,1,1],[8427,1,1],[8428,1,1],[8429,1,1],[8430,1,1],[8431,1,1],[8432,1,1],[8491,0,1],[8602,0,1],[8603,0,1],[8622,0,1],[8653,0,1],[8654,0,1],[8655,0,1],[8708,0,1],[8713,0,1],[8716,0,1],[8740,0,1],[8742,0,1],[8769,0,1],[8772,0,1],[8775,0,1],[8777,0,1],[8800,0,1],[8802,0,1],[8813,0,1],[8814,0,1],[8815,0,1],[8816,0,1],[8817,0,1],[8820,0,1],[8821,0,1],[8824,0,1],[8825,0,1],[8832,0,1],[8833,0,1],[8836,0,1],[8837,0,1],[8840,0,1],[8841,0,1],[8876,0,1],[8877,0,1],[8878,0,1],[8879,0,1],[8928,0,1],[8929,0,1],[8930,0,1],[8931,0,1],[8938,0,1],[8939,0,1],[8940,0,1],[8941,0,1],[10972,0,1],[11503,1,1],[11504,1,1],[11505,1,1],[11647,1,1],[11744,1,1],[11745,1,1],[11746,1,1],[11747,1,1],[11748,1,1],[11749,1,1],[11750,1,1],[11751,1,1],[11752,1,1],[11753,1,1],[11754,1,1],[11755,1,1],[11756,1,1],[11757,1,1],[11758,1,1],[11759,1,1],[11760,1,1],[11761,1,1],[11762,1,1],[11763,1,1],[11764,1,1],[11765,1,1],[11766,1,1],[11767,1,1],[11768,1,1],[11769,1,1],[11770,1,1],[11771,1,1],[11772,1,1],[11773,1,1],[11774,1,1],[11775,1,1],[12330,1,1],[12331,1,1],[12332,1,1],[12333,1,1],[12334,1,1],[12335,1,1],[12364,0,1],[12366,0,1],[12368,0,1],[12370,0,1],[12372,0,1],[12374,0,1],[12376,0,1],[12378,0,1],[12380,0,1],[12382,0,1],[12384,0,1],[12386,0,1],[12389,0,1],[12391,0,1],[12393,0,1],[12400,0,1],[12401,0,1],[12403,0,1],[12404,0,1],[12406,0,1],[12407,0,1],[12409,0,1],[12410,0,1],[12412,0,1],[12413,0,1],[12436,0,1],[12441,1,1],[12442,1,1],[12443,0,1],[12444,0,1],[12446,0,1],[12460,0,1],[12462,0,1],[12464,0,1],[12466,0,1],[12468,0,1],[12470,0,1],[12472,0,1],[12474,0,1],[12476,0,1],[12478,0,1],[12480,0,1],[12482,0,1],[12485,0,1],[12487,0,1],[12489,0,1],[12496,0,1],[12497,0,1],[12499,0,1],[12500,0,1],[12502,0,1],[12503,0,1],[12505,0,1],[12506,0,1],[12508,0,1],[12509,0,1],[12532,0,1],[12535,0,1],[12536,0,1],[12537,0,1],[12538,0,1],[12542,0,1],[12595,1,1],[12597,1,1],[12598,1,1],[12602,1,1],[12603,1,1],[12604,1,1],[12605,1,1],[12606,1,1],[12607,1,1],[12623,1,1],[12624,1,1],[12625,1,1],[12626,1,1],[12627,1,1],[12628,1,1],[12629,1,1],[12630,1,1],[12631,1,1],[12632,1,1],[12633,1,1],[12634,1,1],[12635,1,1],[12636,1,1],[12637,1,1],[12638,1,1],[12639,1,1],[12640,1,1],[12641,1,1],[12642,1,1],[12643,1,1],[12910,0,1],[12911,0,1],[12912,0,1],[12913,0,1],[12914,0,1],[12915,0,1],[12916,0,1],[12917,0,1],[12918,0,1],[12919,0,1],[12920,0,1],[12921,0,1],[12922,0,1],[12923,0,1],[12924,0,1],[12925,0,1],[12926,0,1],[13060,0,1],[13063,0,1],[13072,0,1],[13086,0,1],[13089,0,1],[13106,0,1],[13115,0,1],[13120,0,1],[13131,0,1],[13134,0,1],[42607,1,1],[42612,1,1],[42613,1,1],[42614,1,1],[42615,1,1],[42616,1,1],[42617,1,1],[42618,1,1],[42619,1,1],[42620,1,1],[42621,1,1],[42654,1,1],[42655,1,1],[42736,1,1],[42737,1,1],[43014,1,1],[43052,1,1],[43204,1,1],[43232,1,1],[43233,1,1],[43234,1,1],[43235,1,1],[43236,1,1],[43237,1,1],[43238,1,1],[43239,1,1],[43240,1,1],[43241,1,1],[43242,1,1],[43243,1,1],[43244,1,1],[43245,1,1],[43246,1,1],[43247,1,1],[43248,1,1],[43249,1,1],[43307,1,1],[43308,1,1],[43309,1,1],[43347,1,1],[43443,1,1],[43456,1,1],[43696,1,1],[43698,1,1],[43699,1,1],[43700,1,1],[43703,1,1],[43704,1,1],[43710,1,1],[43711,1,1],[43713,1,1],[43766,1,1],[44013,1,1],[44032,0,1],[44033,0,2],[44034,0,2],[44035,0,2],[44036,0,2],[44037,0,2],[44038,0,2],[44039,0,2],[44040,0,2],[44041,0,2],[44042,0,2],[44043,0,2],[44044,0,2],[44045,0,2],[44046,0,2],[44047,0,2],[44048,0,2],[44049,0,2],[44050,0,2],[44051,0,2],[44052,0,2],[44053,0,2],[44054,0,2],[44055,0,2],[44056,0,2],[44057,0,2],[44058,0,2],[44059,0,2],[44060,0,1],[44061,0,2],[44062,0,2],[44063,0,2],[44064,0,2],[44065,0,2],[44066,0,2],[44067,0,2],[44068,0,2],[44069,0,2],[44070,0,2],[44071,0,2],[44072,0,2],[44073,0,2],[44074,0,2],[44075,0,2],[44076,0,2],[44077,0,2],[44078,0,2],[44079,0,2],[44080,0,2],[44081,0,2],[44082,0,2],[44083,0,2],[44084,0,2],[44085,0,2],[44086,0,2],[44087,0,2],[44088,0,1],[44089,0,2],[44090,0,2],[44091,0,2],[44092,0,2],[44093,0,2],[44094,0,2],[44095,0,2],[44096,0,2],[44097,0,2],[44098,0,2],[44099,0,2],[44100,0,2],[44101,0,2],[44102,0,2],[44103,0,2],[44104,0,2],[44105,0,2],[44106,0,2],[44107,0,2],[44108,0,2],[44109,0,2],[44110,0,2],[44111,0,2],[44112,0,2],[44113,0,2],[44114,0,2],[44115,0,2],[44116,0,1],[44117,0,2],[44118,0,2],[44119,0,2],[44120,0,2],[44121,0,2],[44122,0,2],[44123,0,2],[44124,0,2],[44125,0,2],[44126,0,2],[44127,0,2],[44128,0,2],[44129,0,2],[44130,0,2],[44131,0,2],[44132,0,2],[44133,0,2],[44134,0,2],[44135,0,2],[44136,0,2],[44137,0,2],[44138,0,2],[44139,0,2],[44140,0,2],[44141,0,2],[44142,0,2],[44143,0,2],[44144,0,1],[44145,0,2],[44146,0,2],[44147,0,2],[44148,0,2],[44149,0,2],[44150,0,2],[44151,0,2],[44152,0,2],[44153,0,2],[44154,0,2],[44155,0,2],[44156,0,2],[44157,0,2],[44158,0,2],[44159,0,2],[44160,0,2],[44161,0,2],[44162,0,2],[44163,0,2],[44164,0,2],[44165,0,2],[44166,0,2],[44167,0,2],[44168,0,2],[44169,0,2],[44170,0,2],[44171,0,2],[44172,0,1],[44173,0,2],[44174,0,2],[44175,0,2],[44176,0,2],[44177,0,2],[44178,0,2],[44179,0,2],[44180,0,2],[44181,0,2],[44182,0,2],[44183,0,2],[44184,0,2],[44185,0,2],[44186,0,2],[44187,0,2],[44188,0,2],[44189,0,2],[44190,0,2],[44191,0,2],[44192,0,2],[44193,0,2],[44194,0,2],[44195,0,2],[44196,0,2],[44197,0,2],[44198,0,2],[44199,0,2],[44200,0,1],[44201,0,2],[44202,0,2],[44203,0,2],[44204,0,2],[44205,0,2],[44206,0,2],[44207,0,2],[44208,0,2],[44209,0,2],[44210,0,2],[44211,0,2],[44212,0,2],[44213,0,2],[44214,0,2],[44215,0,2],[44216,0,2],[44217,0,2],[44218,0,2],[44219,0,2],[44220,0,2],[44221,0,2],[44222,0,2],[44223,0,2],[44224,0,2],[44225,0,2],[44226,0,2],[44227,0,2],[44228,0,1],[44229,0,2],[44230,0,2],[44231,0,2],[44232,0,2],[44233,0,2],[44234,0,2],[44235,0,2],[44236,0,2],[44237,0,2],[44238,0,2],[44239,0,2],[44240,0,2],[44241,0,2],[44242,0,2],[44243,0,2],[44244,0,2],[44245,0,2],[44246,0,2],[44247,0,2],[44248,0,2],[44249,0,2],[44250,0,2],[44251,0,2],[44252,0,2],[44253,0,2],[44254,0,2],[44255,0,2],[44256,0,1],[44257,0,2],[44258,0,2],[44259,0,2],[44260,0,2],[44261,0,2],[44262,0,2],[44263,0,2],[44264,0,2],[44265,0,2],[44266,0,2],[44267,0,2],[44268,0,2],[44269,0,2],[44270,0,2],[44271,0,2],[44272,0,2],[44273,0,2],[44274,0,2],[44275,0,2],[44276,0,2],[44277,0,2],[44278,0,2],[44279,0,2],[44280,0,2],[44281,0,2],[44282,0,2],[44283,0,2],[44284,0,1],[44285,0,2],[44286,0,2],[44287,0,2],[44288,0,2],[44289,0,2],[44290,0,2],[44291,0,2],[44292,0,2],[44293,0,2],[44294,0,2],[44295,0,2],[44296,0,2],[44297,0,2],[44298,0,2],[44299,0,2],[44300,0,2],[44301,0,2],[44302,0,2],[44303,0,2],[44304,0,2],[44305,0,2],[44306,0,2],[44307,0,2],[44308,0,2],[44309,0,2],[44310,0,2],[44311,0,2],[44312,0,1],[44313,0,2],[44314,0,2],[44315,0,2],[44316,0,2],[44317,0,2],[44318,0,2],[44319,0,2],[44320,0,2],[44321,0,2],[44322,0,2],[44323,0,2],[44324,0,2],[44325,0,2],[44326,0,2],[44327,0,2],[44328,0,2],[44329,0,2],[44330,0,2],[44331,0,2],[44332,0,2],[44333,0,2],[44334,0,2],[44335,0,2],[44336,0,2],[44337,0,2],[44338,0,2],[44339,0,2],[44340,0,1],[44341,0,2],[44342,0,2],[44343,0,2],[44344,0,2],[44345,0,2],[44346,0,2],[44347,0,2],[44348,0,2],[44349,0,2],[44350,0,2],[44351,0,2],[44352,0,2],[44353,0,2],[44354,0,2],[44355,0,2],[44356,0,2],[44357,0,2],[44358,0,2],[44359,0,2],[44360,0,2],[44361,0,2],[44362,0,2],[44363,0,2],[44364,0,2],[44365,0,2],[44366,0,2],[44367,0,2],[44368,0,1],[44369,0,2],[44370,0,2],[44371,0,2],[44372,0,2],[44373,0,2],[44374,0,2],[44375,0,2],[44376,0,2],[44377,0,2],[44378,0,2],[44379,0,2],[44380,0,2],[44381,0,2],[44382,0,2],[44383,0,2],[44384,0,2],[44385,0,2],[44386,0,2],[44387,0,2],[44388,0,2],[44389,0,2],[44390,0,2],[44391,0,2],[44392,0,2],[44393,0,2],[44394,0,2],[44395,0,2],[44396,0,1],[44397,0,2],[44398,0,2],[44399,0,2],[44400,0,2],[44401,0,2],[44402,0,2],[44403,0,2],[44404,0,2],[44405,0,2],[44406,0,2],[44407,0,2],[44408,0,2],[44409,0,2],[44410,0,2],[44411,0,2],[44412,0,2],[44413,0,2],[44414,0,2],[44415,0,2],[44416,0,2],[44417,0,2],[44418,0,2],[44419,0,2],[44420,0,2],[44421,0,2],[44422,0,2],[44423,0,2],[44424,0,1],[44425,0,2],[44426,0,2],[44427,0,2],[44428,0,2],[44429,0,2],[44430,0,2],[44431,0,2],[44432,0,2],[44433,0,2],[44434,0,2],[44435,0,2],[44436,0,2],[44437,0,2],[44438,0,2],[44439,0,2],[44440,0,2],[44441,0,2],[44442,0,2],[44443,0,2],[44444,0,2],[44445,0,2],[44446,0,2],[44447,0,2],[44448,0,2],[44449,0,2],[44450,0,2],[44451,0,2],[44452,0,1],[44453,0,2],[44454,0,2],[44455,0,2],[44456,0,2],[44457,0,2],[44458,0,2],[44459,0,2],[44460,0,2],[44461,0,2],[44462,0,2],[44463,0,2],[44464,0,2],[44465,0,2],[44466,0,2],[44467,0,2],[44468,0,2],[44469,0,2],[44470,0,2],[44471,0,2],[44472,0,2],[44473,0,2],[44474,0,2],[44475,0,2],[44476,0,2],[44477,0,2],[44478,0,2],[44479,0,2],[44480,0,1],[44481,0,2],[44482,0,2],[44483,0,2],[44484,0,2],[44485,0,2],[44486,0,2],[44487,0,2],[44488,0,2],[44489,0,2],[44490,0,2],[44491,0,2],[44492,0,2],[44493,0,2],[44494,0,2],[44495,0,2],[44496,0,2],[44497,0,2],[44498,0,2],[44499,0,2],[44500,0,2],[44501,0,2],[44502,0,2],[44503,0,2],[44504,0,2],[44505,0,2],[44506,0,2],[44507,0,2],[44508,0,1],[44509,0,2],[44510,0,2],[44511,0,2],[44512,0,2],[44513,0,2],[44514,0,2],[44515,0,2],[44516,0,2],[44517,0,2],[44518,0,2],[44519,0,2],[44520,0,2],[44521,0,2],[44522,0,2],[44523,0,2],[44524,0,2],[44525,0,2],[44526,0,2],[44527,0,2],[44528,0,2],[44529,0,2],[44530,0,2],[44531,0,2],[44532,0,2],[44533,0,2],[44534,0,2],[44535,0,2],[44536,0,1],[44537,0,2],[44538,0,2],[44539,0,2],[44540,0,2],[44541,0,2],[44542,0,2],[44543,0,2],[44544,0,2],[44545,0,2],[44546,0,2],[44547,0,2],[44548,0,2],[44549,0,2],[44550,0,2],[44551,0,2],[44552,0,2],[44553,0,2],[44554,0,2],[44555,0,2],[44556,0,2],[44557,0,2],[44558,0,2],[44559,0,2],[44560,0,2],[44561,0,2],[44562,0,2],[44563,0,2],[44564,0,1],[44565,0,2],[44566,0,2],[44567,0,2],[44568,0,2],[44569,0,2],[44570,0,2],[44571,0,2],[44572,0,2],[44573,0,2],[44574,0,2],[44575,0,2],[44576,0,2],[44577,0,2],[44578,0,2],[44579,0,2],[44580,0,2],[44581,0,2],[44582,0,2],[44583,0,2],[44584,0,2],[44585,0,2],[44586,0,2],[44587,0,2],[44588,0,2],[44589,0,2],[44590,0,2],[44591,0,2],[44592,0,1],[44593,0,2],[44594,0,2],[44595,0,2],[44596,0,2],[44597,0,2],[44598,0,2],[44599,0,2],[44600,0,2],[44601,0,2],[44602,0,2],[44603,0,2],[44604,0,2],[44605,0,2],[44606,0,2],[44607,0,2],[44608,0,2],[44609,0,2],[44610,0,2],[44611,0,2],[44612,0,2],[44613,0,2],[44614,0,2],[44615,0,2],[44616,0,2],[44617,0,2],[44618,0,2],[44619,0,2],[44620,0,1],[44621,0,2],[44622,0,2],[44623,0,2],[44624,0,2],[44625,0,2],[44626,0,2],[44627,0,2],[44628,0,2],[44629,0,2],[44630,0,2],[44631,0,2],[44632,0,2],[44633,0,2],[44634,0,2],[44635,0,2],[44636,0,2],[44637,0,2],[44638,0,2],[44639,0,2],[44640,0,2],[44641,0,2],[44642,0,2],[44643,0,2],[44644,0,2],[44645,0,2],[44646,0,2],[44647,0,2],[44648,0,1],[44649,0,2],[44650,0,2],[44651,0,2],[44652,0,2],[44653,0,2],[44654,0,2],[44655,0,2],[44656,0,2],[44657,0,2],[44658,0,2],[44659,0,2],[44660,0,2],[44661,0,2],[44662,0,2],[44663,0,2],[44664,0,2],[44665,0,2],[44666,0,2],[44667,0,2],[44668,0,2],[44669,0,2],[44670,0,2],[44671,0,2],[44672,0,2],[44673,0,2],[44674,0,2],[44675,0,2],[44676,0,1],[44677,0,2],[44678,0,2],[44679,0,2],[44680,0,2],[44681,0,2],[44682,0,2],[44683,0,2],[44684,0,2],[44685,0,2],[44686,0,2],[44687,0,2],[44688,0,2],[44689,0,2],[44690,0,2],[44691,0,2],[44692,0,2],[44693,0,2],[44694,0,2],[44695,0,2],[44696,0,2],[44697,0,2],[44698,0,2],[44699,0,2],[44700,0,2],[44701,0,2],[44702,0,2],[44703,0,2],[44704,0,1],[44705,0,2],[44706,0,2],[44707,0,2],[44708,0,2],[44709,0,2],[44710,0,2],[44711,0,2],[44712,0,2],[44713,0,2],[44714,0,2],[44715,0,2],[44716,0,2],[44717,0,2],[44718,0,2],[44719,0,2],[44720,0,2],[44721,0,2],[44722,0,2],[44723,0,2],[44724,0,2],[44725,0,2],[44726,0,2],[44727,0,2],[44728,0,2],[44729,0,2],[44730,0,2],[44731,0,2],[44732,0,1],[44733,0,2],[44734,0,2],[44735,0,2],[44736,0,2],[44737,0,2],[44738,0,2],[44739,0,2],[44740,0,2],[44741,0,2],[44742,0,2],[44743,0,2],[44744,0,2],[44745,0,2],[44746,0,2],[44747,0,2],[44748,0,2],[44749,0,2],[44750,0,2],[44751,0,2],[44752,0,2],[44753,0,2],[44754,0,2],[44755,0,2],[44756,0,2],[44757,0,2],[44758,0,2],[44759,0,2],[44760,0,1],[44761,0,2],[44762,0,2],[44763,0,2],[44764,0,2],[44765,0,2],[44766,0,2],[44767,0,2],[44768,0,2],[44769,0,2],[44770,0,2],[44771,0,2],[44772,0,2],[44773,0,2],[44774,0,2],[44775,0,2],[44776,0,2],[44777,0,2],[44778,0,2],[44779,0,2],[44780,0,2],[44781,0,2],[44782,0,2],[44783,0,2],[44784,0,2],[44785,0,2],[44786,0,2],[44787,0,2],[44788,0,1],[44789,0,2],[44790,0,2],[44791,0,2],[44792,0,2],[44793,0,2],[44794,0,2],[44795,0,2],[44796,0,2],[44797,0,2],[44798,0,2],[44799,0,2],[44800,0,2],[44801,0,2],[44802,0,2],[44803,0,2],[44804,0,2],[44805,0,2],[44806,0,2],[44807,0,2],[44808,0,2],[44809,0,2],[44810,0,2],[44811,0,2],[44812,0,2],[44813,0,2],[44814,0,2],[44815,0,2],[44816,0,1],[44817,0,2],[44818,0,2],[44819,0,2],[44820,0,2],[44821,0,2],[44822,0,2],[44823,0,2],[44824,0,2],[44825,0,2],[44826,0,2],[44827,0,2],[44828,0,2],[44829,0,2],[44830,0,2],[44831,0,2],[44832,0,2],[44833,0,2],[44834,0,2],[44835,0,2],[44836,0,2],[44837,0,2],[44838,0,2],[44839,0,2],[44840,0,2],[44841,0,2],[44842,0,2],[44843,0,2],[44844,0,1],[44845,0,2],[44846,0,2],[44847,0,2],[44848,0,2],[44849,0,2],[44850,0,2],[44851,0,2],[44852,0,2],[44853,0,2],[44854,0,2],[44855,0,2],[44856,0,2],[44857,0,2],[44858,0,2],[44859,0,2],[44860,0,2],[44861,0,2],[44862,0,2],[44863,0,2],[44864,0,2],[44865,0,2],[44866,0,2],[44867,0,2],[44868,0,2],[44869,0,2],[44870,0,2],[44871,0,2],[44872,0,1],[44873,0,2],[44874,0,2],[44875,0,2],[44876,0,2],[44877,0,2],[44878,0,2],[44879,0,2],[44880,0,2],[44881,0,2],[44882,0,2],[44883,0,2],[44884,0,2],[44885,0,2],[44886,0,2],[44887,0,2],[44888,0,2],[44889,0,2],[44890,0,2],[44891,0,2],[44892,0,2],[44893,0,2],[44894,0,2],[44895,0,2],[44896,0,2],[44897,0,2],[44898,0,2],[44899,0,2],[44900,0,1],[44901,0,2],[44902,0,2],[44903,0,2],[44904,0,2],[44905,0,2],[44906,0,2],[44907,0,2],[44908,0,2],[44909,0,2],[44910,0,2],[44911,0,2],[44912,0,2],[44913,0,2],[44914,0,2],[44915,0,2],[44916,0,2],[44917,0,2],[44918,0,2],[44919,0,2],[44920,0,2],[44921,0,2],[44922,0,2],[44923,0,2],[44924,0,2],[44925,0,2],[44926,0,2],[44927,0,2],[44928,0,1],[44929,0,2],[44930,0,2],[44931,0,2],[44932,0,2],[44933,0,2],[44934,0,2],[44935,0,2],[44936,0,2],[44937,0,2],[44938,0,2],[44939,0,2],[44940,0,2],[44941,0,2],[44942,0,2],[44943,0,2],[44944,0,2],[44945,0,2],[44946,0,2],[44947,0,2],[44948,0,2],[44949,0,2],[44950,0,2],[44951,0,2],[44952,0,2],[44953,0,2],[44954,0,2],[44955,0,2],[44956,0,1],[44957,0,2],[44958,0,2],[44959,0,2],[44960,0,2],[44961,0,2],[44962,0,2],[44963,0,2],[44964,0,2],[44965,0,2],[44966,0,2],[44967,0,2],[44968,0,2],[44969,0,2],[44970,0,2],[44971,0,2],[44972,0,2],[44973,0,2],[44974,0,2],[44975,0,2],[44976,0,2],[44977,0,2],[44978,0,2],[44979,0,2],[44980,0,2],[44981,0,2],[44982,0,2],[44983,0,2],[44984,0,1],[44985,0,2],[44986,0,2],[44987,0,2],[44988,0,2],[44989,0,2],[44990,0,2],[44991,0,2],[44992,0,2],[44993,0,2],[44994,0,2],[44995,0,2],[44996,0,2],[44997,0,2],[44998,0,2],[44999,0,2],[45000,0,2],[45001,0,2],[45002,0,2],[45003,0,2],[45004,0,2],[45005,0,2],[45006,0,2],[45007,0,2],[45008,0,2],[45009,0,2],[45010,0,2],[45011,0,2],[45012,0,1],[45013,0,2],[45014,0,2],[45015,0,2],[45016,0,2],[45017,0,2],[45018,0,2],[45019,0,2],[45020,0,2],[45021,0,2],[45022,0,2],[45023,0,2],[45024,0,2],[45025,0,2],[45026,0,2],[45027,0,2],[45028,0,2],[45029,0,2],[45030,0,2],[45031,0,2],[45032,0,2],[45033,0,2],[45034,0,2],[45035,0,2],[45036,0,2],[45037,0,2],[45038,0,2],[45039,0,2],[45040,0,1],[45041,0,2],[45042,0,2],[45043,0,2],[45044,0,2],[45045,0,2],[45046,0,2],[45047,0,2],[45048,0,2],[45049,0,2],[45050,0,2],[45051,0,2],[45052,0,2],[45053,0,2],[45054,0,2],[45055,0,2],[45056,0,2],[45057,0,2],[45058,0,2],[45059,0,2],[45060,0,2],[45061,0,2],[45062,0,2],[45063,0,2],[45064,0,2],[45065,0,2],[45066,0,2],[45067,0,2],[45068,0,1],[45069,0,2],[45070,0,2],[45071,0,2],[45072,0,2],[45073,0,2],[45074,0,2],[45075,0,2],[45076,0,2],[45077,0,2],[45078,0,2],[45079,0,2],[45080,0,2],[45081,0,2],[45082,0,2],[45083,0,2],[45084,0,2],[45085,0,2],[45086,0,2],[45087,0,2],[45088,0,2],[45089,0,2],[45090,0,2],[45091,0,2],[45092,0,2],[45093,0,2],[45094,0,2],[45095,0,2],[45096,0,1],[45097,0,2],[45098,0,2],[45099,0,2],[45100,0,2],[45101,0,2],[45102,0,2],[45103,0,2],[45104,0,2],[45105,0,2],[45106,0,2],[45107,0,2],[45108,0,2],[45109,0,2],[45110,0,2],[45111,0,2],[45112,0,2],[45113,0,2],[45114,0,2],[45115,0,2],[45116,0,2],[45117,0,2],[45118,0,2],[45119,0,2],[45120,0,2],[45121,0,2],[45122,0,2],[45123,0,2],[45124,0,1],[45125,0,2],[45126,0,2],[45127,0,2],[45128,0,2],[45129,0,2],[45130,0,2],[45131,0,2],[45132,0,2],[45133,0,2],[45134,0,2],[45135,0,2],[45136,0,2],[45137,0,2],[45138,0,2],[45139,0,2],[45140,0,2],[45141,0,2],[45142,0,2],[45143,0,2],[45144,0,2],[45145,0,2],[45146,0,2],[45147,0,2],[45148,0,2],[45149,0,2],[45150,0,2],[45151,0,2],[45152,0,1],[45153,0,2],[45154,0,2],[45155,0,2],[45156,0,2],[45157,0,2],[45158,0,2],[45159,0,2],[45160,0,2],[45161,0,2],[45162,0,2],[45163,0,2],[45164,0,2],[45165,0,2],[45166,0,2],[45167,0,2],[45168,0,2],[45169,0,2],[45170,0,2],[45171,0,2],[45172,0,2],[45173,0,2],[45174,0,2],[45175,0,2],[45176,0,2],[45177,0,2],[45178,0,2],[45179,0,2],[45180,0,1],[45181,0,2],[45182,0,2],[45183,0,2],[45184,0,2],[45185,0,2],[45186,0,2],[45187,0,2],[45188,0,2],[45189,0,2],[45190,0,2],[45191,0,2],[45192,0,2],[45193,0,2],[45194,0,2],[45195,0,2],[45196,0,2],[45197,0,2],[45198,0,2],[45199,0,2],[45200,0,2],[45201,0,2],[45202,0,2],[45203,0,2],[45204,0,2],[45205,0,2],[45206,0,2],[45207,0,2],[45208,0,1],[45209,0,2],[45210,0,2],[45211,0,2],[45212,0,2],[45213,0,2],[45214,0,2],[45215,0,2],[45216,0,2],[45217,0,2],[45218,0,2],[45219,0,2],[45220,0,2],[45221,0,2],[45222,0,2],[45223,0,2],[45224,0,2],[45225,0,2],[45226,0,2],[45227,0,2],[45228,0,2],[45229,0,2],[45230,0,2],[45231,0,2],[45232,0,2],[45233,0,2],[45234,0,2],[45235,0,2],[45236,0,1],[45237,0,2],[45238,0,2],[45239,0,2],[45240,0,2],[45241,0,2],[45242,0,2],[45243,0,2],[45244,0,2],[45245,0,2],[45246,0,2],[45247,0,2],[45248,0,2],[45249,0,2],[45250,0,2],[45251,0,2],[45252,0,2],[45253,0,2],[45254,0,2],[45255,0,2],[45256,0,2],[45257,0,2],[45258,0,2],[45259,0,2],[45260,0,2],[45261,0,2],[45262,0,2],[45263,0,2],[45264,0,1],[45265,0,2],[45266,0,2],[45267,0,2],[45268,0,2],[45269,0,2],[45270,0,2],[45271,0,2],[45272,0,2],[45273,0,2],[45274,0,2],[45275,0,2],[45276,0,2],[45277,0,2],[45278,0,2],[45279,0,2],[45280,0,2],[45281,0,2],[45282,0,2],[45283,0,2],[45284,0,2],[45285,0,2],[45286,0,2],[45287,0,2],[45288,0,2],[45289,0,2],[45290,0,2],[45291,0,2],[45292,0,1],[45293,0,2],[45294,0,2],[45295,0,2],[45296,0,2],[45297,0,2],[45298,0,2],[45299,0,2],[45300,0,2],[45301,0,2],[45302,0,2],[45303,0,2],[45304,0,2],[45305,0,2],[45306,0,2],[45307,0,2],[45308,0,2],[45309,0,2],[45310,0,2],[45311,0,2],[45312,0,2],[45313,0,2],[45314,0,2],[45315,0,2],[45316,0,2],[45317,0,2],[45318,0,2],[45319,0,2],[45320,0,1],[45321,0,2],[45322,0,2],[45323,0,2],[45324,0,2],[45325,0,2],[45326,0,2],[45327,0,2],[45328,0,2],[45329,0,2],[45330,0,2],[45331,0,2],[45332,0,2],[45333,0,2],[45334,0,2],[45335,0,2],[45336,0,2],[45337,0,2],[45338,0,2],[45339,0,2],[45340,0,2],[45341,0,2],[45342,0,2],[45343,0,2],[45344,0,2],[45345,0,2],[45346,0,2],[45347,0,2],[45348,0,1],[45349,0,2],[45350,0,2],[45351,0,2],[45352,0,2],[45353,0,2],[45354,0,2],[45355,0,2],[45356,0,2],[45357,0,2],[45358,0,2],[45359,0,2],[45360,0,2],[45361,0,2],[45362,0,2],[45363,0,2],[45364,0,2],[45365,0,2],[45366,0,2],[45367,0,2],[45368,0,2],[45369,0,2],[45370,0,2],[45371,0,2],[45372,0,2],[45373,0,2],[45374,0,2],[45375,0,2],[45376,0,1],[45377,0,2],[45378,0,2],[45379,0,2],[45380,0,2],[45381,0,2],[45382,0,2],[45383,0,2],[45384,0,2],[45385,0,2],[45386,0,2],[45387,0,2],[45388,0,2],[45389,0,2],[45390,0,2],[45391,0,2],[45392,0,2],[45393,0,2],[45394,0,2],[45395,0,2],[45396,0,2],[45397,0,2],[45398,0,2],[45399,0,2],[45400,0,2],[45401,0,2],[45402,0,2],[45403,0,2],[45404,0,1],[45405,0,2],[45406,0,2],[45407,0,2],[45408,0,2],[45409,0,2],[45410,0,2],[45411,0,2],[45412,0,2],[45413,0,2],[45414,0,2],[45415,0,2],[45416,0,2],[45417,0,2],[45418,0,2],[45419,0,2],[45420,0,2],[45421,0,2],[45422,0,2],[45423,0,2],[45424,0,2],[45425,0,2],[45426,0,2],[45427,0,2],[45428,0,2],[45429,0,2],[45430,0,2],[45431,0,2],[45432,0,1],[45433,0,2],[45434,0,2],[45435,0,2],[45436,0,2],[45437,0,2],[45438,0,2],[45439,0,2],[45440,0,2],[45441,0,2],[45442,0,2],[45443,0,2],[45444,0,2],[45445,0,2],[45446,0,2],[45447,0,2],[45448,0,2],[45449,0,2],[45450,0,2],[45451,0,2],[45452,0,2],[45453,0,2],[45454,0,2],[45455,0,2],[45456,0,2],[45457,0,2],[45458,0,2],[45459,0,2],[45460,0,1],[45461,0,2],[45462,0,2],[45463,0,2],[45464,0,2],[45465,0,2],[45466,0,2],[45467,0,2],[45468,0,2],[45469,0,2],[45470,0,2],[45471,0,2],[45472,0,2],[45473,0,2],[45474,0,2],[45475,0,2],[45476,0,2],[45477,0,2],[45478,0,2],[45479,0,2],[45480,0,2],[45481,0,2],[45482,0,2],[45483,0,2],[45484,0,2],[45485,0,2],[45486,0,2],[45487,0,2],[45488,0,1],[45489,0,2],[45490,0,2],[45491,0,2],[45492,0,2],[45493,0,2],[45494,0,2],[45495,0,2],[45496,0,2],[45497,0,2],[45498,0,2],[45499,0,2],[45500,0,2],[45501,0,2],[45502,0,2],[45503,0,2],[45504,0,2],[45505,0,2],[45506,0,2],[45507,0,2],[45508,0,2],[45509,0,2],[45510,0,2],[45511,0,2],[45512,0,2],[45513,0,2],[45514,0,2],[45515,0,2],[45516,0,1],[45517,0,2],[45518,0,2],[45519,0,2],[45520,0,2],[45521,0,2],[45522,0,2],[45523,0,2],[45524,0,2],[45525,0,2],[45526,0,2],[45527,0,2],[45528,0,2],[45529,0,2],[45530,0,2],[45531,0,2],[45532,0,2],[45533,0,2],[45534,0,2],[45535,0,2],[45536,0,2],[45537,0,2],[45538,0,2],[45539,0,2],[45540,0,2],[45541,0,2],[45542,0,2],[45543,0,2],[45544,0,1],[45545,0,2],[45546,0,2],[45547,0,2],[45548,0,2],[45549,0,2],[45550,0,2],[45551,0,2],[45552,0,2],[45553,0,2],[45554,0,2],[45555,0,2],[45556,0,2],[45557,0,2],[45558,0,2],[45559,0,2],[45560,0,2],[45561,0,2],[45562,0,2],[45563,0,2],[45564,0,2],[45565,0,2],[45566,0,2],[45567,0,2],[45568,0,2],[45569,0,2],[45570,0,2],[45571,0,2],[45572,0,1],[45573,0,2],[45574,0,2],[45575,0,2],[45576,0,2],[45577,0,2],[45578,0,2],[45579,0,2],[45580,0,2],[45581,0,2],[45582,0,2],[45583,0,2],[45584,0,2],[45585,0,2],[45586,0,2],[45587,0,2],[45588,0,2],[45589,0,2],[45590,0,2],[45591,0,2],[45592,0,2],[45593,0,2],[45594,0,2],[45595,0,2],[45596,0,2],[45597,0,2],[45598,0,2],[45599,0,2],[45600,0,1],[45601,0,2],[45602,0,2],[45603,0,2],[45604,0,2],[45605,0,2],[45606,0,2],[45607,0,2],[45608,0,2],[45609,0,2],[45610,0,2],[45611,0,2],[45612,0,2],[45613,0,2],[45614,0,2],[45615,0,2],[45616,0,2],[45617,0,2],[45618,0,2],[45619,0,2],[45620,0,2],[45621,0,2],[45622,0,2],[45623,0,2],[45624,0,2],[45625,0,2],[45626,0,2],[45627,0,2],[45628,0,1],[45629,0,2],[45630,0,2],[45631,0,2],[45632,0,2],[45633,0,2],[45634,0,2],[45635,0,2],[45636,0,2],[45637,0,2],[45638,0,2],[45639,0,2],[45640,0,2],[45641,0,2],[45642,0,2],[45643,0,2],[45644,0,2],[45645,0,2],[45646,0,2],[45647,0,2],[45648,0,2],[45649,0,2],[45650,0,2],[45651,0,2],[45652,0,2],[45653,0,2],[45654,0,2],[45655,0,2],[45656,0,1],[45657,0,2],[45658,0,2],[45659,0,2],[45660,0,2],[45661,0,2],[45662,0,2],[45663,0,2],[45664,0,2],[45665,0,2],[45666,0,2],[45667,0,2],[45668,0,2],[45669,0,2],[45670,0,2],[45671,0,2],[45672,0,2],[45673,0,2],[45674,0,2],[45675,0,2],[45676,0,2],[45677,0,2],[45678,0,2],[45679,0,2],[45680,0,2],[45681,0,2],[45682,0,2],[45683,0,2],[45684,0,1],[45685,0,2],[45686,0,2],[45687,0,2],[45688,0,2],[45689,0,2],[45690,0,2],[45691,0,2],[45692,0,2],[45693,0,2],[45694,0,2],[45695,0,2],[45696,0,2],[45697,0,2],[45698,0,2],[45699,0,2],[45700,0,2],[45701,0,2],[45702,0,2],[45703,0,2],[45704,0,2],[45705,0,2],[45706,0,2],[45707,0,2],[45708,0,2],[45709,0,2],[45710,0,2],[45711,0,2],[45712,0,1],[45713,0,2],[45714,0,2],[45715,0,2],[45716,0,2],[45717,0,2],[45718,0,2],[45719,0,2],[45720,0,2],[45721,0,2],[45722,0,2],[45723,0,2],[45724,0,2],[45725,0,2],[45726,0,2],[45727,0,2],[45728,0,2],[45729,0,2],[45730,0,2],[45731,0,2],[45732,0,2],[45733,0,2],[45734,0,2],[45735,0,2],[45736,0,2],[45737,0,2],[45738,0,2],[45739,0,2],[45740,0,1],[45741,0,2],[45742,0,2],[45743,0,2],[45744,0,2],[45745,0,2],[45746,0,2],[45747,0,2],[45748,0,2],[45749,0,2],[45750,0,2],[45751,0,2],[45752,0,2],[45753,0,2],[45754,0,2],[45755,0,2],[45756,0,2],[45757,0,2],[45758,0,2],[45759,0,2],[45760,0,2],[45761,0,2],[45762,0,2],[45763,0,2],[45764,0,2],[45765,0,2],[45766,0,2],[45767,0,2],[45768,0,1],[45769,0,2],[45770,0,2],[45771,0,2],[45772,0,2],[45773,0,2],[45774,0,2],[45775,0,2],[45776,0,2],[45777,0,2],[45778,0,2],[45779,0,2],[45780,0,2],[45781,0,2],[45782,0,2],[45783,0,2],[45784,0,2],[45785,0,2],[45786,0,2],[45787,0,2],[45788,0,2],[45789,0,2],[45790,0,2],[45791,0,2],[45792,0,2],[45793,0,2],[45794,0,2],[45795,0,2],[45796,0,1],[45797,0,2],[45798,0,2],[45799,0,2],[45800,0,2],[45801,0,2],[45802,0,2],[45803,0,2],[45804,0,2],[45805,0,2],[45806,0,2],[45807,0,2],[45808,0,2],[45809,0,2],[45810,0,2],[45811,0,2],[45812,0,2],[45813,0,2],[45814,0,2],[45815,0,2],[45816,0,2],[45817,0,2],[45818,0,2],[45819,0,2],[45820,0,2],[45821,0,2],[45822,0,2],[45823,0,2],[45824,0,1],[45825,0,2],[45826,0,2],[45827,0,2],[45828,0,2],[45829,0,2],[45830,0,2],[45831,0,2],[45832,0,2],[45833,0,2],[45834,0,2],[45835,0,2],[45836,0,2],[45837,0,2],[45838,0,2],[45839,0,2],[45840,0,2],[45841,0,2],[45842,0,2],[45843,0,2],[45844,0,2],[45845,0,2],[45846,0,2],[45847,0,2],[45848,0,2],[45849,0,2],[45850,0,2],[45851,0,2],[45852,0,1],[45853,0,2],[45854,0,2],[45855,0,2],[45856,0,2],[45857,0,2],[45858,0,2],[45859,0,2],[45860,0,2],[45861,0,2],[45862,0,2],[45863,0,2],[45864,0,2],[45865,0,2],[45866,0,2],[45867,0,2],[45868,0,2],[45869,0,2],[45870,0,2],[45871,0,2],[45872,0,2],[45873,0,2],[45874,0,2],[45875,0,2],[45876,0,2],[45877,0,2],[45878,0,2],[45879,0,2],[45880,0,1],[45881,0,2],[45882,0,2],[45883,0,2],[45884,0,2],[45885,0,2],[45886,0,2],[45887,0,2],[45888,0,2],[45889,0,2],[45890,0,2],[45891,0,2],[45892,0,2],[45893,0,2],[45894,0,2],[45895,0,2],[45896,0,2],[45897,0,2],[45898,0,2],[45899,0,2],[45900,0,2],[45901,0,2],[45902,0,2],[45903,0,2],[45904,0,2],[45905,0,2],[45906,0,2],[45907,0,2],[45908,0,1],[45909,0,2],[45910,0,2],[45911,0,2],[45912,0,2],[45913,0,2],[45914,0,2],[45915,0,2],[45916,0,2],[45917,0,2],[45918,0,2],[45919,0,2],[45920,0,2],[45921,0,2],[45922,0,2],[45923,0,2],[45924,0,2],[45925,0,2],[45926,0,2],[45927,0,2],[45928,0,2],[45929,0,2],[45930,0,2],[45931,0,2],[45932,0,2],[45933,0,2],[45934,0,2],[45935,0,2],[45936,0,1],[45937,0,2],[45938,0,2],[45939,0,2],[45940,0,2],[45941,0,2],[45942,0,2],[45943,0,2],[45944,0,2],[45945,0,2],[45946,0,2],[45947,0,2],[45948,0,2],[45949,0,2],[45950,0,2],[45951,0,2],[45952,0,2],[45953,0,2],[45954,0,2],[45955,0,2],[45956,0,2],[45957,0,2],[45958,0,2],[45959,0,2],[45960,0,2],[45961,0,2],[45962,0,2],[45963,0,2],[45964,0,1],[45965,0,2],[45966,0,2],[45967,0,2],[45968,0,2],[45969,0,2],[45970,0,2],[45971,0,2],[45972,0,2],[45973,0,2],[45974,0,2],[45975,0,2],[45976,0,2],[45977,0,2],[45978,0,2],[45979,0,2],[45980,0,2],[45981,0,2],[45982,0,2],[45983,0,2],[45984,0,2],[45985,0,2],[45986,0,2],[45987,0,2],[45988,0,2],[45989,0,2],[45990,0,2],[45991,0,2],[45992,0,1],[45993,0,2],[45994,0,2],[45995,0,2],[45996,0,2],[45997,0,2],[45998,0,2],[45999,0,2],[46000,0,2],[46001,0,2],[46002,0,2],[46003,0,2],[46004,0,2],[46005,0,2],[46006,0,2],[46007,0,2],[46008,0,2],[46009,0,2],[46010,0,2],[46011,0,2],[46012,0,2],[46013,0,2],[46014,0,2],[46015,0,2],[46016,0,2],[46017,0,2],[46018,0,2],[46019,0,2],[46020,0,1],[46021,0,2],[46022,0,2],[46023,0,2],[46024,0,2],[46025,0,2],[46026,0,2],[46027,0,2],[46028,0,2],[46029,0,2],[46030,0,2],[46031,0,2],[46032,0,2],[46033,0,2],[46034,0,2],[46035,0,2],[46036,0,2],[46037,0,2],[46038,0,2],[46039,0,2],[46040,0,2],[46041,0,2],[46042,0,2],[46043,0,2],[46044,0,2],[46045,0,2],[46046,0,2],[46047,0,2],[46048,0,1],[46049,0,2],[46050,0,2],[46051,0,2],[46052,0,2],[46053,0,2],[46054,0,2],[46055,0,2],[46056,0,2],[46057,0,2],[46058,0,2],[46059,0,2],[46060,0,2],[46061,0,2],[46062,0,2],[46063,0,2],[46064,0,2],[46065,0,2],[46066,0,2],[46067,0,2],[46068,0,2],[46069,0,2],[46070,0,2],[46071,0,2],[46072,0,2],[46073,0,2],[46074,0,2],[46075,0,2],[46076,0,1],[46077,0,2],[46078,0,2],[46079,0,2],[46080,0,2],[46081,0,2],[46082,0,2],[46083,0,2],[46084,0,2],[46085,0,2],[46086,0,2],[46087,0,2],[46088,0,2],[46089,0,2],[46090,0,2],[46091,0,2],[46092,0,2],[46093,0,2],[46094,0,2],[46095,0,2],[46096,0,2],[46097,0,2],[46098,0,2],[46099,0,2],[46100,0,2],[46101,0,2],[46102,0,2],[46103,0,2],[46104,0,1],[46105,0,2],[46106,0,2],[46107,0,2],[46108,0,2],[46109,0,2],[46110,0,2],[46111,0,2],[46112,0,2],[46113,0,2],[46114,0,2],[46115,0,2],[46116,0,2],[46117,0,2],[46118,0,2],[46119,0,2],[46120,0,2],[46121,0,2],[46122,0,2],[46123,0,2],[46124,0,2],[46125,0,2],[46126,0,2],[46127,0,2],[46128,0,2],[46129,0,2],[46130,0,2],[46131,0,2],[46132,0,1],[46133,0,2],[46134,0,2],[46135,0,2],[46136,0,2],[46137,0,2],[46138,0,2],[46139,0,2],[46140,0,2],[46141,0,2],[46142,0,2],[46143,0,2],[46144,0,2],[46145,0,2],[46146,0,2],[46147,0,2],[46148,0,2],[46149,0,2],[46150,0,2],[46151,0,2],[46152,0,2],[46153,0,2],[46154,0,2],[46155,0,2],[46156,0,2],[46157,0,2],[46158,0,2],[46159,0,2],[46160,0,1],[46161,0,2],[46162,0,2],[46163,0,2],[46164,0,2],[46165,0,2],[46166,0,2],[46167,0,2],[46168,0,2],[46169,0,2],[46170,0,2],[46171,0,2],[46172,0,2],[46173,0,2],[46174,0,2],[46175,0,2],[46176,0,2],[46177,0,2],[46178,0,2],[46179,0,2],[46180,0,2],[46181,0,2],[46182,0,2],[46183,0,2],[46184,0,2],[46185,0,2],[46186,0,2],[46187,0,2],[46188,0,1],[46189,0,2],[46190,0,2],[46191,0,2],[46192,0,2],[46193,0,2],[46194,0,2],[46195,0,2],[46196,0,2],[46197,0,2],[46198,0,2],[46199,0,2],[46200,0,2],[46201,0,2],[46202,0,2],[46203,0,2],[46204,0,2],[46205,0,2],[46206,0,2],[46207,0,2],[46208,0,2],[46209,0,2],[46210,0,2],[46211,0,2],[46212,0,2],[46213,0,2],[46214,0,2],[46215,0,2],[46216,0,1],[46217,0,2],[46218,0,2],[46219,0,2],[46220,0,2],[46221,0,2],[46222,0,2],[46223,0,2],[46224,0,2],[46225,0,2],[46226,0,2],[46227,0,2],[46228,0,2],[46229,0,2],[46230,0,2],[46231,0,2],[46232,0,2],[46233,0,2],[46234,0,2],[46235,0,2],[46236,0,2],[46237,0,2],[46238,0,2],[46239,0,2],[46240,0,2],[46241,0,2],[46242,0,2],[46243,0,2],[46244,0,1],[46245,0,2],[46246,0,2],[46247,0,2],[46248,0,2],[46249,0,2],[46250,0,2],[46251,0,2],[46252,0,2],[46253,0,2],[46254,0,2],[46255,0,2],[46256,0,2],[46257,0,2],[46258,0,2],[46259,0,2],[46260,0,2],[46261,0,2],[46262,0,2],[46263,0,2],[46264,0,2],[46265,0,2],[46266,0,2],[46267,0,2],[46268,0,2],[46269,0,2],[46270,0,2],[46271,0,2],[46272,0,1],[46273,0,2],[46274,0,2],[46275,0,2],[46276,0,2],[46277,0,2],[46278,0,2],[46279,0,2],[46280,0,2],[46281,0,2],[46282,0,2],[46283,0,2],[46284,0,2],[46285,0,2],[46286,0,2],[46287,0,2],[46288,0,2],[46289,0,2],[46290,0,2],[46291,0,2],[46292,0,2],[46293,0,2],[46294,0,2],[46295,0,2],[46296,0,2],[46297,0,2],[46298,0,2],[46299,0,2],[46300,0,1],[46301,0,2],[46302,0,2],[46303,0,2],[46304,0,2],[46305,0,2],[46306,0,2],[46307,0,2],[46308,0,2],[46309,0,2],[46310,0,2],[46311,0,2],[46312,0,2],[46313,0,2],[46314,0,2],[46315,0,2],[46316,0,2],[46317,0,2],[46318,0,2],[46319,0,2],[46320,0,2],[46321,0,2],[46322,0,2],[46323,0,2],[46324,0,2],[46325,0,2],[46326,0,2],[46327,0,2],[46328,0,1],[46329,0,2],[46330,0,2],[46331,0,2],[46332,0,2],[46333,0,2],[46334,0,2],[46335,0,2],[46336,0,2],[46337,0,2],[46338,0,2],[46339,0,2],[46340,0,2],[46341,0,2],[46342,0,2],[46343,0,2],[46344,0,2],[46345,0,2],[46346,0,2],[46347,0,2],[46348,0,2],[46349,0,2],[46350,0,2],[46351,0,2],[46352,0,2],[46353,0,2],[46354,0,2],[46355,0,2],[46356,0,1],[46357,0,2],[46358,0,2],[46359,0,2],[46360,0,2],[46361,0,2],[46362,0,2],[46363,0,2],[46364,0,2],[46365,0,2],[46366,0,2],[46367,0,2],[46368,0,2],[46369,0,2],[46370,0,2],[46371,0,2],[46372,0,2],[46373,0,2],[46374,0,2],[46375,0,2],[46376,0,2],[46377,0,2],[46378,0,2],[46379,0,2],[46380,0,2],[46381,0,2],[46382,0,2],[46383,0,2],[46384,0,1],[46385,0,2],[46386,0,2],[46387,0,2],[46388,0,2],[46389,0,2],[46390,0,2],[46391,0,2],[46392,0,2],[46393,0,2],[46394,0,2],[46395,0,2],[46396,0,2],[46397,0,2],[46398,0,2],[46399,0,2],[46400,0,2],[46401,0,2],[46402,0,2],[46403,0,2],[46404,0,2],[46405,0,2],[46406,0,2],[46407,0,2],[46408,0,2],[46409,0,2],[46410,0,2],[46411,0,2],[46412,0,1],[46413,0,2],[46414,0,2],[46415,0,2],[46416,0,2],[46417,0,2],[46418,0,2],[46419,0,2],[46420,0,2],[46421,0,2],[46422,0,2],[46423,0,2],[46424,0,2],[46425,0,2],[46426,0,2],[46427,0,2],[46428,0,2],[46429,0,2],[46430,0,2],[46431,0,2],[46432,0,2],[46433,0,2],[46434,0,2],[46435,0,2],[46436,0,2],[46437,0,2],[46438,0,2],[46439,0,2],[46440,0,1],[46441,0,2],[46442,0,2],[46443,0,2],[46444,0,2],[46445,0,2],[46446,0,2],[46447,0,2],[46448,0,2],[46449,0,2],[46450,0,2],[46451,0,2],[46452,0,2],[46453,0,2],[46454,0,2],[46455,0,2],[46456,0,2],[46457,0,2],[46458,0,2],[46459,0,2],[46460,0,2],[46461,0,2],[46462,0,2],[46463,0,2],[46464,0,2],[46465,0,2],[46466,0,2],[46467,0,2],[46468,0,1],[46469,0,2],[46470,0,2],[46471,0,2],[46472,0,2],[46473,0,2],[46474,0,2],[46475,0,2],[46476,0,2],[46477,0,2],[46478,0,2],[46479,0,2],[46480,0,2],[46481,0,2],[46482,0,2],[46483,0,2],[46484,0,2],[46485,0,2],[46486,0,2],[46487,0,2],[46488,0,2],[46489,0,2],[46490,0,2],[46491,0,2],[46492,0,2],[46493,0,2],[46494,0,2],[46495,0,2],[46496,0,1],[46497,0,2],[46498,0,2],[46499,0,2],[46500,0,2],[46501,0,2],[46502,0,2],[46503,0,2],[46504,0,2],[46505,0,2],[46506,0,2],[46507,0,2],[46508,0,2],[46509,0,2],[46510,0,2],[46511,0,2],[46512,0,2],[46513,0,2],[46514,0,2],[46515,0,2],[46516,0,2],[46517,0,2],[46518,0,2],[46519,0,2],[46520,0,2],[46521,0,2],[46522,0,2],[46523,0,2],[46524,0,1],[46525,0,2],[46526,0,2],[46527,0,2],[46528,0,2],[46529,0,2],[46530,0,2],[46531,0,2],[46532,0,2],[46533,0,2],[46534,0,2],[46535,0,2],[46536,0,2],[46537,0,2],[46538,0,2],[46539,0,2],[46540,0,2],[46541,0,2],[46542,0,2],[46543,0,2],[46544,0,2],[46545,0,2],[46546,0,2],[46547,0,2],[46548,0,2],[46549,0,2],[46550,0,2],[46551,0,2],[46552,0,1],[46553,0,2],[46554,0,2],[46555,0,2],[46556,0,2],[46557,0,2],[46558,0,2],[46559,0,2],[46560,0,2],[46561,0,2],[46562,0,2],[46563,0,2],[46564,0,2],[46565,0,2],[46566,0,2],[46567,0,2],[46568,0,2],[46569,0,2],[46570,0,2],[46571,0,2],[46572,0,2],[46573,0,2],[46574,0,2],[46575,0,2],[46576,0,2],[46577,0,2],[46578,0,2],[46579,0,2],[46580,0,1],[46581,0,2],[46582,0,2],[46583,0,2],[46584,0,2],[46585,0,2],[46586,0,2],[46587,0,2],[46588,0,2],[46589,0,2],[46590,0,2],[46591,0,2],[46592,0,2],[46593,0,2],[46594,0,2],[46595,0,2],[46596,0,2],[46597,0,2],[46598,0,2],[46599,0,2],[46600,0,2],[46601,0,2],[46602,0,2],[46603,0,2],[46604,0,2],[46605,0,2],[46606,0,2],[46607,0,2],[46608,0,1],[46609,0,2],[46610,0,2],[46611,0,2],[46612,0,2],[46613,0,2],[46614,0,2],[46615,0,2],[46616,0,2],[46617,0,2],[46618,0,2],[46619,0,2],[46620,0,2],[46621,0,2],[46622,0,2],[46623,0,2],[46624,0,2],[46625,0,2],[46626,0,2],[46627,0,2],[46628,0,2],[46629,0,2],[46630,0,2],[46631,0,2],[46632,0,2],[46633,0,2],[46634,0,2],[46635,0,2],[46636,0,1],[46637,0,2],[46638,0,2],[46639,0,2],[46640,0,2],[46641,0,2],[46642,0,2],[46643,0,2],[46644,0,2],[46645,0,2],[46646,0,2],[46647,0,2],[46648,0,2],[46649,0,2],[46650,0,2],[46651,0,2],[46652,0,2],[46653,0,2],[46654,0,2],[46655,0,2],[46656,0,2],[46657,0,2],[46658,0,2],[46659,0,2],[46660,0,2],[46661,0,2],[46662,0,2],[46663,0,2],[46664,0,1],[46665,0,2],[46666,0,2],[46667,0,2],[46668,0,2],[46669,0,2],[46670,0,2],[46671,0,2],[46672,0,2],[46673,0,2],[46674,0,2],[46675,0,2],[46676,0,2],[46677,0,2],[46678,0,2],[46679,0,2],[46680,0,2],[46681,0,2],[46682,0,2],[46683,0,2],[46684,0,2],[46685,0,2],[46686,0,2],[46687,0,2],[46688,0,2],[46689,0,2],[46690,0,2],[46691,0,2],[46692,0,1],[46693,0,2],[46694,0,2],[46695,0,2],[46696,0,2],[46697,0,2],[46698,0,2],[46699,0,2],[46700,0,2],[46701,0,2],[46702,0,2],[46703,0,2],[46704,0,2],[46705,0,2],[46706,0,2],[46707,0,2],[46708,0,2],[46709,0,2],[46710,0,2],[46711,0,2],[46712,0,2],[46713,0,2],[46714,0,2],[46715,0,2],[46716,0,2],[46717,0,2],[46718,0,2],[46719,0,2],[46720,0,1],[46721,0,2],[46722,0,2],[46723,0,2],[46724,0,2],[46725,0,2],[46726,0,2],[46727,0,2],[46728,0,2],[46729,0,2],[46730,0,2],[46731,0,2],[46732,0,2],[46733,0,2],[46734,0,2],[46735,0,2],[46736,0,2],[46737,0,2],[46738,0,2],[46739,0,2],[46740,0,2],[46741,0,2],[46742,0,2],[46743,0,2],[46744,0,2],[46745,0,2],[46746,0,2],[46747,0,2],[46748,0,1],[46749,0,2],[46750,0,2],[46751,0,2],[46752,0,2],[46753,0,2],[46754,0,2],[46755,0,2],[46756,0,2],[46757,0,2],[46758,0,2],[46759,0,2],[46760,0,2],[46761,0,2],[46762,0,2],[46763,0,2],[46764,0,2],[46765,0,2],[46766,0,2],[46767,0,2],[46768,0,2],[46769,0,2],[46770,0,2],[46771,0,2],[46772,0,2],[46773,0,2],[46774,0,2],[46775,0,2],[46776,0,1],[46777,0,2],[46778,0,2],[46779,0,2],[46780,0,2],[46781,0,2],[46782,0,2],[46783,0,2],[46784,0,2],[46785,0,2],[46786,0,2],[46787,0,2],[46788,0,2],[46789,0,2],[46790,0,2],[46791,0,2],[46792,0,2],[46793,0,2],[46794,0,2],[46795,0,2],[46796,0,2],[46797,0,2],[46798,0,2],[46799,0,2],[46800,0,2],[46801,0,2],[46802,0,2],[46803,0,2],[46804,0,1],[46805,0,2],[46806,0,2],[46807,0,2],[46808,0,2],[46809,0,2],[46810,0,2],[46811,0,2],[46812,0,2],[46813,0,2],[46814,0,2],[46815,0,2],[46816,0,2],[46817,0,2],[46818,0,2],[46819,0,2],[46820,0,2],[46821,0,2],[46822,0,2],[46823,0,2],[46824,0,2],[46825,0,2],[46826,0,2],[46827,0,2],[46828,0,2],[46829,0,2],[46830,0,2],[46831,0,2],[46832,0,1],[46833,0,2],[46834,0,2],[46835,0,2],[46836,0,2],[46837,0,2],[46838,0,2],[46839,0,2],[46840,0,2],[46841,0,2],[46842,0,2],[46843,0,2],[46844,0,2],[46845,0,2],[46846,0,2],[46847,0,2],[46848,0,2],[46849,0,2],[46850,0,2],[46851,0,2],[46852,0,2],[46853,0,2],[46854,0,2],[46855,0,2],[46856,0,2],[46857,0,2],[46858,0,2],[46859,0,2],[46860,0,1],[46861,0,2],[46862,0,2],[46863,0,2],[46864,0,2],[46865,0,2],[46866,0,2],[46867,0,2],[46868,0,2],[46869,0,2],[46870,0,2],[46871,0,2],[46872,0,2],[46873,0,2],[46874,0,2],[46875,0,2],[46876,0,2],[46877,0,2],[46878,0,2],[46879,0,2],[46880,0,2],[46881,0,2],[46882,0,2],[46883,0,2],[46884,0,2],[46885,0,2],[46886,0,2],[46887,0,2],[46888,0,1],[46889,0,2],[46890,0,2],[46891,0,2],[46892,0,2],[46893,0,2],[46894,0,2],[46895,0,2],[46896,0,2],[46897,0,2],[46898,0,2],[46899,0,2],[46900,0,2],[46901,0,2],[46902,0,2],[46903,0,2],[46904,0,2],[46905,0,2],[46906,0,2],[46907,0,2],[46908,0,2],[46909,0,2],[46910,0,2],[46911,0,2],[46912,0,2],[46913,0,2],[46914,0,2],[46915,0,2],[46916,0,1],[46917,0,2],[46918,0,2],[46919,0,2],[46920,0,2],[46921,0,2],[46922,0,2],[46923,0,2],[46924,0,2],[46925,0,2],[46926,0,2],[46927,0,2],[46928,0,2],[46929,0,2],[46930,0,2],[46931,0,2],[46932,0,2],[46933,0,2],[46934,0,2],[46935,0,2],[46936,0,2],[46937,0,2],[46938,0,2],[46939,0,2],[46940,0,2],[46941,0,2],[46942,0,2],[46943,0,2],[46944,0,1],[46945,0,2],[46946,0,2],[46947,0,2],[46948,0,2],[46949,0,2],[46950,0,2],[46951,0,2],[46952,0,2],[46953,0,2],[46954,0,2],[46955,0,2],[46956,0,2],[46957,0,2],[46958,0,2],[46959,0,2],[46960,0,2],[46961,0,2],[46962,0,2],[46963,0,2],[46964,0,2],[46965,0,2],[46966,0,2],[46967,0,2],[46968,0,2],[46969,0,2],[46970,0,2],[46971,0,2],[46972,0,1],[46973,0,2],[46974,0,2],[46975,0,2],[46976,0,2],[46977,0,2],[46978,0,2],[46979,0,2],[46980,0,2],[46981,0,2],[46982,0,2],[46983,0,2],[46984,0,2],[46985,0,2],[46986,0,2],[46987,0,2],[46988,0,2],[46989,0,2],[46990,0,2],[46991,0,2],[46992,0,2],[46993,0,2],[46994,0,2],[46995,0,2],[46996,0,2],[46997,0,2],[46998,0,2],[46999,0,2],[47000,0,1],[47001,0,2],[47002,0,2],[47003,0,2],[47004,0,2],[47005,0,2],[47006,0,2],[47007,0,2],[47008,0,2],[47009,0,2],[47010,0,2],[47011,0,2],[47012,0,2],[47013,0,2],[47014,0,2],[47015,0,2],[47016,0,2],[47017,0,2],[47018,0,2],[47019,0,2],[47020,0,2],[47021,0,2],[47022,0,2],[47023,0,2],[47024,0,2],[47025,0,2],[47026,0,2],[47027,0,2],[47028,0,1],[47029,0,2],[47030,0,2],[47031,0,2],[47032,0,2],[47033,0,2],[47034,0,2],[47035,0,2],[47036,0,2],[47037,0,2],[47038,0,2],[47039,0,2],[47040,0,2],[47041,0,2],[47042,0,2],[47043,0,2],[47044,0,2],[47045,0,2],[47046,0,2],[47047,0,2],[47048,0,2],[47049,0,2],[47050,0,2],[47051,0,2],[47052,0,2],[47053,0,2],[47054,0,2],[47055,0,2],[47056,0,1],[47057,0,2],[47058,0,2],[47059,0,2],[47060,0,2],[47061,0,2],[47062,0,2],[47063,0,2],[47064,0,2],[47065,0,2],[47066,0,2],[47067,0,2],[47068,0,2],[47069,0,2],[47070,0,2],[47071,0,2],[47072,0,2],[47073,0,2],[47074,0,2],[47075,0,2],[47076,0,2],[47077,0,2],[47078,0,2],[47079,0,2],[47080,0,2],[47081,0,2],[47082,0,2],[47083,0,2],[47084,0,1],[47085,0,2],[47086,0,2],[47087,0,2],[47088,0,2],[47089,0,2],[47090,0,2],[47091,0,2],[47092,0,2],[47093,0,2],[47094,0,2],[47095,0,2],[47096,0,2],[47097,0,2],[47098,0,2],[47099,0,2],[47100,0,2],[47101,0,2],[47102,0,2],[47103,0,2],[47104,0,2],[47105,0,2],[47106,0,2],[47107,0,2],[47108,0,2],[47109,0,2],[47110,0,2],[47111,0,2],[47112,0,1],[47113,0,2],[47114,0,2],[47115,0,2],[47116,0,2],[47117,0,2],[47118,0,2],[47119,0,2],[47120,0,2],[47121,0,2],[47122,0,2],[47123,0,2],[47124,0,2],[47125,0,2],[47126,0,2],[47127,0,2],[47128,0,2],[47129,0,2],[47130,0,2],[47131,0,2],[47132,0,2],[47133,0,2],[47134,0,2],[47135,0,2],[47136,0,2],[47137,0,2],[47138,0,2],[47139,0,2],[47140,0,1],[47141,0,2],[47142,0,2],[47143,0,2],[47144,0,2],[47145,0,2],[47146,0,2],[47147,0,2],[47148,0,2],[47149,0,2],[47150,0,2],[47151,0,2],[47152,0,2],[47153,0,2],[47154,0,2],[47155,0,2],[47156,0,2],[47157,0,2],[47158,0,2],[47159,0,2],[47160,0,2],[47161,0,2],[47162,0,2],[47163,0,2],[47164,0,2],[47165,0,2],[47166,0,2],[47167,0,2],[47168,0,1],[47169,0,2],[47170,0,2],[47171,0,2],[47172,0,2],[47173,0,2],[47174,0,2],[47175,0,2],[47176,0,2],[47177,0,2],[47178,0,2],[47179,0,2],[47180,0,2],[47181,0,2],[47182,0,2],[47183,0,2],[47184,0,2],[47185,0,2],[47186,0,2],[47187,0,2],[47188,0,2],[47189,0,2],[47190,0,2],[47191,0,2],[47192,0,2],[47193,0,2],[47194,0,2],[47195,0,2],[47196,0,1],[47197,0,2],[47198,0,2],[47199,0,2],[47200,0,2],[47201,0,2],[47202,0,2],[47203,0,2],[47204,0,2],[47205,0,2],[47206,0,2],[47207,0,2],[47208,0,2],[47209,0,2],[47210,0,2],[47211,0,2],[47212,0,2],[47213,0,2],[47214,0,2],[47215,0,2],[47216,0,2],[47217,0,2],[47218,0,2],[47219,0,2],[47220,0,2],[47221,0,2],[47222,0,2],[47223,0,2],[47224,0,1],[47225,0,2],[47226,0,2],[47227,0,2],[47228,0,2],[47229,0,2],[47230,0,2],[47231,0,2],[47232,0,2],[47233,0,2],[47234,0,2],[47235,0,2],[47236,0,2],[47237,0,2],[47238,0,2],[47239,0,2],[47240,0,2],[47241,0,2],[47242,0,2],[47243,0,2],[47244,0,2],[47245,0,2],[47246,0,2],[47247,0,2],[47248,0,2],[47249,0,2],[47250,0,2],[47251,0,2],[47252,0,1],[47253,0,2],[47254,0,2],[47255,0,2],[47256,0,2],[47257,0,2],[47258,0,2],[47259,0,2],[47260,0,2],[47261,0,2],[47262,0,2],[47263,0,2],[47264,0,2],[47265,0,2],[47266,0,2],[47267,0,2],[47268,0,2],[47269,0,2],[47270,0,2],[47271,0,2],[47272,0,2],[47273,0,2],[47274,0,2],[47275,0,2],[47276,0,2],[47277,0,2],[47278,0,2],[47279,0,2],[47280,0,1],[47281,0,2],[47282,0,2],[47283,0,2],[47284,0,2],[47285,0,2],[47286,0,2],[47287,0,2],[47288,0,2],[47289,0,2],[47290,0,2],[47291,0,2],[47292,0,2],[47293,0,2],[47294,0,2],[47295,0,2],[47296,0,2],[47297,0,2],[47298,0,2],[47299,0,2],[47300,0,2],[47301,0,2],[47302,0,2],[47303,0,2],[47304,0,2],[47305,0,2],[47306,0,2],[47307,0,2],[47308,0,1],[47309,0,2],[47310,0,2],[47311,0,2],[47312,0,2],[47313,0,2],[47314,0,2],[47315,0,2],[47316,0,2],[47317,0,2],[47318,0,2],[47319,0,2],[47320,0,2],[47321,0,2],[47322,0,2],[47323,0,2],[47324,0,2],[47325,0,2],[47326,0,2],[47327,0,2],[47328,0,2],[47329,0,2],[47330,0,2],[47331,0,2],[47332,0,2],[47333,0,2],[47334,0,2],[47335,0,2],[47336,0,1],[47337,0,2],[47338,0,2],[47339,0,2],[47340,0,2],[47341,0,2],[47342,0,2],[47343,0,2],[47344,0,2],[47345,0,2],[47346,0,2],[47347,0,2],[47348,0,2],[47349,0,2],[47350,0,2],[47351,0,2],[47352,0,2],[47353,0,2],[47354,0,2],[47355,0,2],[47356,0,2],[47357,0,2],[47358,0,2],[47359,0,2],[47360,0,2],[47361,0,2],[47362,0,2],[47363,0,2],[47364,0,1],[47365,0,2],[47366,0,2],[47367,0,2],[47368,0,2],[47369,0,2],[47370,0,2],[47371,0,2],[47372,0,2],[47373,0,2],[47374,0,2],[47375,0,2],[47376,0,2],[47377,0,2],[47378,0,2],[47379,0,2],[47380,0,2],[47381,0,2],[47382,0,2],[47383,0,2],[47384,0,2],[47385,0,2],[47386,0,2],[47387,0,2],[47388,0,2],[47389,0,2],[47390,0,2],[47391,0,2],[47392,0,1],[47393,0,2],[47394,0,2],[47395,0,2],[47396,0,2],[47397,0,2],[47398,0,2],[47399,0,2],[47400,0,2],[47401,0,2],[47402,0,2],[47403,0,2],[47404,0,2],[47405,0,2],[47406,0,2],[47407,0,2],[47408,0,2],[47409,0,2],[47410,0,2],[47411,0,2],[47412,0,2],[47413,0,2],[47414,0,2],[47415,0,2],[47416,0,2],[47417,0,2],[47418,0,2],[47419,0,2],[47420,0,1],[47421,0,2],[47422,0,2],[47423,0,2],[47424,0,2],[47425,0,2],[47426,0,2],[47427,0,2],[47428,0,2],[47429,0,2],[47430,0,2],[47431,0,2],[47432,0,2],[47433,0,2],[47434,0,2],[47435,0,2],[47436,0,2],[47437,0,2],[47438,0,2],[47439,0,2],[47440,0,2],[47441,0,2],[47442,0,2],[47443,0,2],[47444,0,2],[47445,0,2],[47446,0,2],[47447,0,2],[47448,0,1],[47449,0,2],[47450,0,2],[47451,0,2],[47452,0,2],[47453,0,2],[47454,0,2],[47455,0,2],[47456,0,2],[47457,0,2],[47458,0,2],[47459,0,2],[47460,0,2],[47461,0,2],[47462,0,2],[47463,0,2],[47464,0,2],[47465,0,2],[47466,0,2],[47467,0,2],[47468,0,2],[47469,0,2],[47470,0,2],[47471,0,2],[47472,0,2],[47473,0,2],[47474,0,2],[47475,0,2],[47476,0,1],[47477,0,2],[47478,0,2],[47479,0,2],[47480,0,2],[47481,0,2],[47482,0,2],[47483,0,2],[47484,0,2],[47485,0,2],[47486,0,2],[47487,0,2],[47488,0,2],[47489,0,2],[47490,0,2],[47491,0,2],[47492,0,2],[47493,0,2],[47494,0,2],[47495,0,2],[47496,0,2],[47497,0,2],[47498,0,2],[47499,0,2],[47500,0,2],[47501,0,2],[47502,0,2],[47503,0,2],[47504,0,1],[47505,0,2],[47506,0,2],[47507,0,2],[47508,0,2],[47509,0,2],[47510,0,2],[47511,0,2],[47512,0,2],[47513,0,2],[47514,0,2],[47515,0,2],[47516,0,2],[47517,0,2],[47518,0,2],[47519,0,2],[47520,0,2],[47521,0,2],[47522,0,2],[47523,0,2],[47524,0,2],[47525,0,2],[47526,0,2],[47527,0,2],[47528,0,2],[47529,0,2],[47530,0,2],[47531,0,2],[47532,0,1],[47533,0,2],[47534,0,2],[47535,0,2],[47536,0,2],[47537,0,2],[47538,0,2],[47539,0,2],[47540,0,2],[47541,0,2],[47542,0,2],[47543,0,2],[47544,0,2],[47545,0,2],[47546,0,2],[47547,0,2],[47548,0,2],[47549,0,2],[47550,0,2],[47551,0,2],[47552,0,2],[47553,0,2],[47554,0,2],[47555,0,2],[47556,0,2],[47557,0,2],[47558,0,2],[47559,0,2],[47560,0,1],[47561,0,2],[47562,0,2],[47563,0,2],[47564,0,2],[47565,0,2],[47566,0,2],[47567,0,2],[47568,0,2],[47569,0,2],[47570,0,2],[47571,0,2],[47572,0,2],[47573,0,2],[47574,0,2],[47575,0,2],[47576,0,2],[47577,0,2],[47578,0,2],[47579,0,2],[47580,0,2],[47581,0,2],[47582,0,2],[47583,0,2],[47584,0,2],[47585,0,2],[47586,0,2],[47587,0,2],[47588,0,1],[47589,0,2],[47590,0,2],[47591,0,2],[47592,0,2],[47593,0,2],[47594,0,2],[47595,0,2],[47596,0,2],[47597,0,2],[47598,0,2],[47599,0,2],[47600,0,2],[47601,0,2],[47602,0,2],[47603,0,2],[47604,0,2],[47605,0,2],[47606,0,2],[47607,0,2],[47608,0,2],[47609,0,2],[47610,0,2],[47611,0,2],[47612,0,2],[47613,0,2],[47614,0,2],[47615,0,2],[47616,0,1],[47617,0,2],[47618,0,2],[47619,0,2],[47620,0,2],[47621,0,2],[47622,0,2],[47623,0,2],[47624,0,2],[47625,0,2],[47626,0,2],[47627,0,2],[47628,0,2],[47629,0,2],[47630,0,2],[47631,0,2],[47632,0,2],[47633,0,2],[47634,0,2],[47635,0,2],[47636,0,2],[47637,0,2],[47638,0,2],[47639,0,2],[47640,0,2],[47641,0,2],[47642,0,2],[47643,0,2],[47644,0,1],[47645,0,2],[47646,0,2],[47647,0,2],[47648,0,2],[47649,0,2],[47650,0,2],[47651,0,2],[47652,0,2],[47653,0,2],[47654,0,2],[47655,0,2],[47656,0,2],[47657,0,2],[47658,0,2],[47659,0,2],[47660,0,2],[47661,0,2],[47662,0,2],[47663,0,2],[47664,0,2],[47665,0,2],[47666,0,2],[47667,0,2],[47668,0,2],[47669,0,2],[47670,0,2],[47671,0,2],[47672,0,1],[47673,0,2],[47674,0,2],[47675,0,2],[47676,0,2],[47677,0,2],[47678,0,2],[47679,0,2],[47680,0,2],[47681,0,2],[47682,0,2],[47683,0,2],[47684,0,2],[47685,0,2],[47686,0,2],[47687,0,2],[47688,0,2],[47689,0,2],[47690,0,2],[47691,0,2],[47692,0,2],[47693,0,2],[47694,0,2],[47695,0,2],[47696,0,2],[47697,0,2],[47698,0,2],[47699,0,2],[47700,0,1],[47701,0,2],[47702,0,2],[47703,0,2],[47704,0,2],[47705,0,2],[47706,0,2],[47707,0,2],[47708,0,2],[47709,0,2],[47710,0,2],[47711,0,2],[47712,0,2],[47713,0,2],[47714,0,2],[47715,0,2],[47716,0,2],[47717,0,2],[47718,0,2],[47719,0,2],[47720,0,2],[47721,0,2],[47722,0,2],[47723,0,2],[47724,0,2],[47725,0,2],[47726,0,2],[47727,0,2],[47728,0,1],[47729,0,2],[47730,0,2],[47731,0,2],[47732,0,2],[47733,0,2],[47734,0,2],[47735,0,2],[47736,0,2],[47737,0,2],[47738,0,2],[47739,0,2],[47740,0,2],[47741,0,2],[47742,0,2],[47743,0,2],[47744,0,2],[47745,0,2],[47746,0,2],[47747,0,2],[47748,0,2],[47749,0,2],[47750,0,2],[47751,0,2],[47752,0,2],[47753,0,2],[47754,0,2],[47755,0,2],[47756,0,1],[47757,0,2],[47758,0,2],[47759,0,2],[47760,0,2],[47761,0,2],[47762,0,2],[47763,0,2],[47764,0,2],[47765,0,2],[47766,0,2],[47767,0,2],[47768,0,2],[47769,0,2],[47770,0,2],[47771,0,2],[47772,0,2],[47773,0,2],[47774,0,2],[47775,0,2],[47776,0,2],[47777,0,2],[47778,0,2],[47779,0,2],[47780,0,2],[47781,0,2],[47782,0,2],[47783,0,2],[47784,0,1],[47785,0,2],[47786,0,2],[47787,0,2],[47788,0,2],[47789,0,2],[47790,0,2],[47791,0,2],[47792,0,2],[47793,0,2],[47794,0,2],[47795,0,2],[47796,0,2],[47797,0,2],[47798,0,2],[47799,0,2],[47800,0,2],[47801,0,2],[47802,0,2],[47803,0,2],[47804,0,2],[47805,0,2],[47806,0,2],[47807,0,2],[47808,0,2],[47809,0,2],[47810,0,2],[47811,0,2],[47812,0,1],[47813,0,2],[47814,0,2],[47815,0,2],[47816,0,2],[47817,0,2],[47818,0,2],[47819,0,2],[47820,0,2],[47821,0,2],[47822,0,2],[47823,0,2],[47824,0,2],[47825,0,2],[47826,0,2],[47827,0,2],[47828,0,2],[47829,0,2],[47830,0,2],[47831,0,2],[47832,0,2],[47833,0,2],[47834,0,2],[47835,0,2],[47836,0,2],[47837,0,2],[47838,0,2],[47839,0,2],[47840,0,1],[47841,0,2],[47842,0,2],[47843,0,2],[47844,0,2],[47845,0,2],[47846,0,2],[47847,0,2],[47848,0,2],[47849,0,2],[47850,0,2],[47851,0,2],[47852,0,2],[47853,0,2],[47854,0,2],[47855,0,2],[47856,0,2],[47857,0,2],[47858,0,2],[47859,0,2],[47860,0,2],[47861,0,2],[47862,0,2],[47863,0,2],[47864,0,2],[47865,0,2],[47866,0,2],[47867,0,2],[47868,0,1],[47869,0,2],[47870,0,2],[47871,0,2],[47872,0,2],[47873,0,2],[47874,0,2],[47875,0,2],[47876,0,2],[47877,0,2],[47878,0,2],[47879,0,2],[47880,0,2],[47881,0,2],[47882,0,2],[47883,0,2],[47884,0,2],[47885,0,2],[47886,0,2],[47887,0,2],[47888,0,2],[47889,0,2],[47890,0,2],[47891,0,2],[47892,0,2],[47893,0,2],[47894,0,2],[47895,0,2],[47896,0,1],[47897,0,2],[47898,0,2],[47899,0,2],[47900,0,2],[47901,0,2],[47902,0,2],[47903,0,2],[47904,0,2],[47905,0,2],[47906,0,2],[47907,0,2],[47908,0,2],[47909,0,2],[47910,0,2],[47911,0,2],[47912,0,2],[47913,0,2],[47914,0,2],[47915,0,2],[47916,0,2],[47917,0,2],[47918,0,2],[47919,0,2],[47920,0,2],[47921,0,2],[47922,0,2],[47923,0,2],[47924,0,1],[47925,0,2],[47926,0,2],[47927,0,2],[47928,0,2],[47929,0,2],[47930,0,2],[47931,0,2],[47932,0,2],[47933,0,2],[47934,0,2],[47935,0,2],[47936,0,2],[47937,0,2],[47938,0,2],[47939,0,2],[47940,0,2],[47941,0,2],[47942,0,2],[47943,0,2],[47944,0,2],[47945,0,2],[47946,0,2],[47947,0,2],[47948,0,2],[47949,0,2],[47950,0,2],[47951,0,2],[47952,0,1],[47953,0,2],[47954,0,2],[47955,0,2],[47956,0,2],[47957,0,2],[47958,0,2],[47959,0,2],[47960,0,2],[47961,0,2],[47962,0,2],[47963,0,2],[47964,0,2],[47965,0,2],[47966,0,2],[47967,0,2],[47968,0,2],[47969,0,2],[47970,0,2],[47971,0,2],[47972,0,2],[47973,0,2],[47974,0,2],[47975,0,2],[47976,0,2],[47977,0,2],[47978,0,2],[47979,0,2],[47980,0,1],[47981,0,2],[47982,0,2],[47983,0,2],[47984,0,2],[47985,0,2],[47986,0,2],[47987,0,2],[47988,0,2],[47989,0,2],[47990,0,2],[47991,0,2],[47992,0,2],[47993,0,2],[47994,0,2],[47995,0,2],[47996,0,2],[47997,0,2],[47998,0,2],[47999,0,2],[48000,0,2],[48001,0,2],[48002,0,2],[48003,0,2],[48004,0,2],[48005,0,2],[48006,0,2],[48007,0,2],[48008,0,1],[48009,0,2],[48010,0,2],[48011,0,2],[48012,0,2],[48013,0,2],[48014,0,2],[48015,0,2],[48016,0,2],[48017,0,2],[48018,0,2],[48019,0,2],[48020,0,2],[48021,0,2],[48022,0,2],[48023,0,2],[48024,0,2],[48025,0,2],[48026,0,2],[48027,0,2],[48028,0,2],[48029,0,2],[48030,0,2],[48031,0,2],[48032,0,2],[48033,0,2],[48034,0,2],[48035,0,2],[48036,0,1],[48037,0,2],[48038,0,2],[48039,0,2],[48040,0,2],[48041,0,2],[48042,0,2],[48043,0,2],[48044,0,2],[48045,0,2],[48046,0,2],[48047,0,2],[48048,0,2],[48049,0,2],[48050,0,2],[48051,0,2],[48052,0,2],[48053,0,2],[48054,0,2],[48055,0,2],[48056,0,2],[48057,0,2],[48058,0,2],[48059,0,2],[48060,0,2],[48061,0,2],[48062,0,2],[48063,0,2],[48064,0,1],[48065,0,2],[48066,0,2],[48067,0,2],[48068,0,2],[48069,0,2],[48070,0,2],[48071,0,2],[48072,0,2],[48073,0,2],[48074,0,2],[48075,0,2],[48076,0,2],[48077,0,2],[48078,0,2],[48079,0,2],[48080,0,2],[48081,0,2],[48082,0,2],[48083,0,2],[48084,0,2],[48085,0,2],[48086,0,2],[48087,0,2],[48088,0,2],[48089,0,2],[48090,0,2],[48091,0,2],[48092,0,1],[48093,0,2],[48094,0,2],[48095,0,2],[48096,0,2],[48097,0,2],[48098,0,2],[48099,0,2],[48100,0,2],[48101,0,2],[48102,0,2],[48103,0,2],[48104,0,2],[48105,0,2],[48106,0,2],[48107,0,2],[48108,0,2],[48109,0,2],[48110,0,2],[48111,0,2],[48112,0,2],[48113,0,2],[48114,0,2],[48115,0,2],[48116,0,2],[48117,0,2],[48118,0,2],[48119,0,2],[48120,0,1],[48121,0,2],[48122,0,2],[48123,0,2],[48124,0,2],[48125,0,2],[48126,0,2],[48127,0,2],[48128,0,2],[48129,0,2],[48130,0,2],[48131,0,2],[48132,0,2],[48133,0,2],[48134,0,2],[48135,0,2],[48136,0,2],[48137,0,2],[48138,0,2],[48139,0,2],[48140,0,2],[48141,0,2],[48142,0,2],[48143,0,2],[48144,0,2],[48145,0,2],[48146,0,2],[48147,0,2],[48148,0,1],[48149,0,2],[48150,0,2],[48151,0,2],[48152,0,2],[48153,0,2],[48154,0,2],[48155,0,2],[48156,0,2],[48157,0,2],[48158,0,2],[48159,0,2],[48160,0,2],[48161,0,2],[48162,0,2],[48163,0,2],[48164,0,2],[48165,0,2],[48166,0,2],[48167,0,2],[48168,0,2],[48169,0,2],[48170,0,2],[48171,0,2],[48172,0,2],[48173,0,2],[48174,0,2],[48175,0,2],[48176,0,1],[48177,0,2],[48178,0,2],[48179,0,2],[48180,0,2],[48181,0,2],[48182,0,2],[48183,0,2],[48184,0,2],[48185,0,2],[48186,0,2],[48187,0,2],[48188,0,2],[48189,0,2],[48190,0,2],[48191,0,2],[48192,0,2],[48193,0,2],[48194,0,2],[48195,0,2],[48196,0,2],[48197,0,2],[48198,0,2],[48199,0,2],[48200,0,2],[48201,0,2],[48202,0,2],[48203,0,2],[48204,0,1],[48205,0,2],[48206,0,2],[48207,0,2],[48208,0,2],[48209,0,2],[48210,0,2],[48211,0,2],[48212,0,2],[48213,0,2],[48214,0,2],[48215,0,2],[48216,0,2],[48217,0,2],[48218,0,2],[48219,0,2],[48220,0,2],[48221,0,2],[48222,0,2],[48223,0,2],[48224,0,2],[48225,0,2],[48226,0,2],[48227,0,2],[48228,0,2],[48229,0,2],[48230,0,2],[48231,0,2],[48232,0,1],[48233,0,2],[48234,0,2],[48235,0,2],[48236,0,2],[48237,0,2],[48238,0,2],[48239,0,2],[48240,0,2],[48241,0,2],[48242,0,2],[48243,0,2],[48244,0,2],[48245,0,2],[48246,0,2],[48247,0,2],[48248,0,2],[48249,0,2],[48250,0,2],[48251,0,2],[48252,0,2],[48253,0,2],[48254,0,2],[48255,0,2],[48256,0,2],[48257,0,2],[48258,0,2],[48259,0,2],[48260,0,1],[48261,0,2],[48262,0,2],[48263,0,2],[48264,0,2],[48265,0,2],[48266,0,2],[48267,0,2],[48268,0,2],[48269,0,2],[48270,0,2],[48271,0,2],[48272,0,2],[48273,0,2],[48274,0,2],[48275,0,2],[48276,0,2],[48277,0,2],[48278,0,2],[48279,0,2],[48280,0,2],[48281,0,2],[48282,0,2],[48283,0,2],[48284,0,2],[48285,0,2],[48286,0,2],[48287,0,2],[48288,0,1],[48289,0,2],[48290,0,2],[48291,0,2],[48292,0,2],[48293,0,2],[48294,0,2],[48295,0,2],[48296,0,2],[48297,0,2],[48298,0,2],[48299,0,2],[48300,0,2],[48301,0,2],[48302,0,2],[48303,0,2],[48304,0,2],[48305,0,2],[48306,0,2],[48307,0,2],[48308,0,2],[48309,0,2],[48310,0,2],[48311,0,2],[48312,0,2],[48313,0,2],[48314,0,2],[48315,0,2],[48316,0,1],[48317,0,2],[48318,0,2],[48319,0,2],[48320,0,2],[48321,0,2],[48322,0,2],[48323,0,2],[48324,0,2],[48325,0,2],[48326,0,2],[48327,0,2],[48328,0,2],[48329,0,2],[48330,0,2],[48331,0,2],[48332,0,2],[48333,0,2],[48334,0,2],[48335,0,2],[48336,0,2],[48337,0,2],[48338,0,2],[48339,0,2],[48340,0,2],[48341,0,2],[48342,0,2],[48343,0,2],[48344,0,1],[48345,0,2],[48346,0,2],[48347,0,2],[48348,0,2],[48349,0,2],[48350,0,2],[48351,0,2],[48352,0,2],[48353,0,2],[48354,0,2],[48355,0,2],[48356,0,2],[48357,0,2],[48358,0,2],[48359,0,2],[48360,0,2],[48361,0,2],[48362,0,2],[48363,0,2],[48364,0,2],[48365,0,2],[48366,0,2],[48367,0,2],[48368,0,2],[48369,0,2],[48370,0,2],[48371,0,2],[48372,0,1],[48373,0,2],[48374,0,2],[48375,0,2],[48376,0,2],[48377,0,2],[48378,0,2],[48379,0,2],[48380,0,2],[48381,0,2],[48382,0,2],[48383,0,2],[48384,0,2],[48385,0,2],[48386,0,2],[48387,0,2],[48388,0,2],[48389,0,2],[48390,0,2],[48391,0,2],[48392,0,2],[48393,0,2],[48394,0,2],[48395,0,2],[48396,0,2],[48397,0,2],[48398,0,2],[48399,0,2],[48400,0,1],[48401,0,2],[48402,0,2],[48403,0,2],[48404,0,2],[48405,0,2],[48406,0,2],[48407,0,2],[48408,0,2],[48409,0,2],[48410,0,2],[48411,0,2],[48412,0,2],[48413,0,2],[48414,0,2],[48415,0,2],[48416,0,2],[48417,0,2],[48418,0,2],[48419,0,2],[48420,0,2],[48421,0,2],[48422,0,2],[48423,0,2],[48424,0,2],[48425,0,2],[48426,0,2],[48427,0,2],[48428,0,1],[48429,0,2],[48430,0,2],[48431,0,2],[48432,0,2],[48433,0,2],[48434,0,2],[48435,0,2],[48436,0,2],[48437,0,2],[48438,0,2],[48439,0,2],[48440,0,2],[48441,0,2],[48442,0,2],[48443,0,2],[48444,0,2],[48445,0,2],[48446,0,2],[48447,0,2],[48448,0,2],[48449,0,2],[48450,0,2],[48451,0,2],[48452,0,2],[48453,0,2],[48454,0,2],[48455,0,2],[48456,0,1],[48457,0,2],[48458,0,2],[48459,0,2],[48460,0,2],[48461,0,2],[48462,0,2],[48463,0,2],[48464,0,2],[48465,0,2],[48466,0,2],[48467,0,2],[48468,0,2],[48469,0,2],[48470,0,2],[48471,0,2],[48472,0,2],[48473,0,2],[48474,0,2],[48475,0,2],[48476,0,2],[48477,0,2],[48478,0,2],[48479,0,2],[48480,0,2],[48481,0,2],[48482,0,2],[48483,0,2],[48484,0,1],[48485,0,2],[48486,0,2],[48487,0,2],[48488,0,2],[48489,0,2],[48490,0,2],[48491,0,2],[48492,0,2],[48493,0,2],[48494,0,2],[48495,0,2],[48496,0,2],[48497,0,2],[48498,0,2],[48499,0,2],[48500,0,2],[48501,0,2],[48502,0,2],[48503,0,2],[48504,0,2],[48505,0,2],[48506,0,2],[48507,0,2],[48508,0,2],[48509,0,2],[48510,0,2],[48511,0,2],[48512,0,1],[48513,0,2],[48514,0,2],[48515,0,2],[48516,0,2],[48517,0,2],[48518,0,2],[48519,0,2],[48520,0,2],[48521,0,2],[48522,0,2],[48523,0,2],[48524,0,2],[48525,0,2],[48526,0,2],[48527,0,2],[48528,0,2],[48529,0,2],[48530,0,2],[48531,0,2],[48532,0,2],[48533,0,2],[48534,0,2],[48535,0,2],[48536,0,2],[48537,0,2],[48538,0,2],[48539,0,2],[48540,0,1],[48541,0,2],[48542,0,2],[48543,0,2],[48544,0,2],[48545,0,2],[48546,0,2],[48547,0,2],[48548,0,2],[48549,0,2],[48550,0,2],[48551,0,2],[48552,0,2],[48553,0,2],[48554,0,2],[48555,0,2],[48556,0,2],[48557,0,2],[48558,0,2],[48559,0,2],[48560,0,2],[48561,0,2],[48562,0,2],[48563,0,2],[48564,0,2],[48565,0,2],[48566,0,2],[48567,0,2],[48568,0,1],[48569,0,2],[48570,0,2],[48571,0,2],[48572,0,2],[48573,0,2],[48574,0,2],[48575,0,2],[48576,0,2],[48577,0,2],[48578,0,2],[48579,0,2],[48580,0,2],[48581,0,2],[48582,0,2],[48583,0,2],[48584,0,2],[48585,0,2],[48586,0,2],[48587,0,2],[48588,0,2],[48589,0,2],[48590,0,2],[48591,0,2],[48592,0,2],[48593,0,2],[48594,0,2],[48595,0,2],[48596,0,1],[48597,0,2],[48598,0,2],[48599,0,2],[48600,0,2],[48601,0,2],[48602,0,2],[48603,0,2],[48604,0,2],[48605,0,2],[48606,0,2],[48607,0,2],[48608,0,2],[48609,0,2],[48610,0,2],[48611,0,2],[48612,0,2],[48613,0,2],[48614,0,2],[48615,0,2],[48616,0,2],[48617,0,2],[48618,0,2],[48619,0,2],[48620,0,2],[48621,0,2],[48622,0,2],[48623,0,2],[48624,0,1],[48625,0,2],[48626,0,2],[48627,0,2],[48628,0,2],[48629,0,2],[48630,0,2],[48631,0,2],[48632,0,2],[48633,0,2],[48634,0,2],[48635,0,2],[48636,0,2],[48637,0,2],[48638,0,2],[48639,0,2],[48640,0,2],[48641,0,2],[48642,0,2],[48643,0,2],[48644,0,2],[48645,0,2],[48646,0,2],[48647,0,2],[48648,0,2],[48649,0,2],[48650,0,2],[48651,0,2],[48652,0,1],[48653,0,2],[48654,0,2],[48655,0,2],[48656,0,2],[48657,0,2],[48658,0,2],[48659,0,2],[48660,0,2],[48661,0,2],[48662,0,2],[48663,0,2],[48664,0,2],[48665,0,2],[48666,0,2],[48667,0,2],[48668,0,2],[48669,0,2],[48670,0,2],[48671,0,2],[48672,0,2],[48673,0,2],[48674,0,2],[48675,0,2],[48676,0,2],[48677,0,2],[48678,0,2],[48679,0,2],[48680,0,1],[48681,0,2],[48682,0,2],[48683,0,2],[48684,0,2],[48685,0,2],[48686,0,2],[48687,0,2],[48688,0,2],[48689,0,2],[48690,0,2],[48691,0,2],[48692,0,2],[48693,0,2],[48694,0,2],[48695,0,2],[48696,0,2],[48697,0,2],[48698,0,2],[48699,0,2],[48700,0,2],[48701,0,2],[48702,0,2],[48703,0,2],[48704,0,2],[48705,0,2],[48706,0,2],[48707,0,2],[48708,0,1],[48709,0,2],[48710,0,2],[48711,0,2],[48712,0,2],[48713,0,2],[48714,0,2],[48715,0,2],[48716,0,2],[48717,0,2],[48718,0,2],[48719,0,2],[48720,0,2],[48721,0,2],[48722,0,2],[48723,0,2],[48724,0,2],[48725,0,2],[48726,0,2],[48727,0,2],[48728,0,2],[48729,0,2],[48730,0,2],[48731,0,2],[48732,0,2],[48733,0,2],[48734,0,2],[48735,0,2],[48736,0,1],[48737,0,2],[48738,0,2],[48739,0,2],[48740,0,2],[48741,0,2],[48742,0,2],[48743,0,2],[48744,0,2],[48745,0,2],[48746,0,2],[48747,0,2],[48748,0,2],[48749,0,2],[48750,0,2],[48751,0,2],[48752,0,2],[48753,0,2],[48754,0,2],[48755,0,2],[48756,0,2],[48757,0,2],[48758,0,2],[48759,0,2],[48760,0,2],[48761,0,2],[48762,0,2],[48763,0,2],[48764,0,1],[48765,0,2],[48766,0,2],[48767,0,2],[48768,0,2],[48769,0,2],[48770,0,2],[48771,0,2],[48772,0,2],[48773,0,2],[48774,0,2],[48775,0,2],[48776,0,2],[48777,0,2],[48778,0,2],[48779,0,2],[48780,0,2],[48781,0,2],[48782,0,2],[48783,0,2],[48784,0,2],[48785,0,2],[48786,0,2],[48787,0,2],[48788,0,2],[48789,0,2],[48790,0,2],[48791,0,2],[48792,0,1],[48793,0,2],[48794,0,2],[48795,0,2],[48796,0,2],[48797,0,2],[48798,0,2],[48799,0,2],[48800,0,2],[48801,0,2],[48802,0,2],[48803,0,2],[48804,0,2],[48805,0,2],[48806,0,2],[48807,0,2],[48808,0,2],[48809,0,2],[48810,0,2],[48811,0,2],[48812,0,2],[48813,0,2],[48814,0,2],[48815,0,2],[48816,0,2],[48817,0,2],[48818,0,2],[48819,0,2],[48820,0,1],[48821,0,2],[48822,0,2],[48823,0,2],[48824,0,2],[48825,0,2],[48826,0,2],[48827,0,2],[48828,0,2],[48829,0,2],[48830,0,2],[48831,0,2],[48832,0,2],[48833,0,2],[48834,0,2],[48835,0,2],[48836,0,2],[48837,0,2],[48838,0,2],[48839,0,2],[48840,0,2],[48841,0,2],[48842,0,2],[48843,0,2],[48844,0,2],[48845,0,2],[48846,0,2],[48847,0,2],[48848,0,1],[48849,0,2],[48850,0,2],[48851,0,2],[48852,0,2],[48853,0,2],[48854,0,2],[48855,0,2],[48856,0,2],[48857,0,2],[48858,0,2],[48859,0,2],[48860,0,2],[48861,0,2],[48862,0,2],[48863,0,2],[48864,0,2],[48865,0,2],[48866,0,2],[48867,0,2],[48868,0,2],[48869,0,2],[48870,0,2],[48871,0,2],[48872,0,2],[48873,0,2],[48874,0,2],[48875,0,2],[48876,0,1],[48877,0,2],[48878,0,2],[48879,0,2],[48880,0,2],[48881,0,2],[48882,0,2],[48883,0,2],[48884,0,2],[48885,0,2],[48886,0,2],[48887,0,2],[48888,0,2],[48889,0,2],[48890,0,2],[48891,0,2],[48892,0,2],[48893,0,2],[48894,0,2],[48895,0,2],[48896,0,2],[48897,0,2],[48898,0,2],[48899,0,2],[48900,0,2],[48901,0,2],[48902,0,2],[48903,0,2],[48904,0,1],[48905,0,2],[48906,0,2],[48907,0,2],[48908,0,2],[48909,0,2],[48910,0,2],[48911,0,2],[48912,0,2],[48913,0,2],[48914,0,2],[48915,0,2],[48916,0,2],[48917,0,2],[48918,0,2],[48919,0,2],[48920,0,2],[48921,0,2],[48922,0,2],[48923,0,2],[48924,0,2],[48925,0,2],[48926,0,2],[48927,0,2],[48928,0,2],[48929,0,2],[48930,0,2],[48931,0,2],[48932,0,1],[48933,0,2],[48934,0,2],[48935,0,2],[48936,0,2],[48937,0,2],[48938,0,2],[48939,0,2],[48940,0,2],[48941,0,2],[48942,0,2],[48943,0,2],[48944,0,2],[48945,0,2],[48946,0,2],[48947,0,2],[48948,0,2],[48949,0,2],[48950,0,2],[48951,0,2],[48952,0,2],[48953,0,2],[48954,0,2],[48955,0,2],[48956,0,2],[48957,0,2],[48958,0,2],[48959,0,2],[48960,0,1],[48961,0,2],[48962,0,2],[48963,0,2],[48964,0,2],[48965,0,2],[48966,0,2],[48967,0,2],[48968,0,2],[48969,0,2],[48970,0,2],[48971,0,2],[48972,0,2],[48973,0,2],[48974,0,2],[48975,0,2],[48976,0,2],[48977,0,2],[48978,0,2],[48979,0,2],[48980,0,2],[48981,0,2],[48982,0,2],[48983,0,2],[48984,0,2],[48985,0,2],[48986,0,2],[48987,0,2],[48988,0,1],[48989,0,2],[48990,0,2],[48991,0,2],[48992,0,2],[48993,0,2],[48994,0,2],[48995,0,2],[48996,0,2],[48997,0,2],[48998,0,2],[48999,0,2],[49000,0,2],[49001,0,2],[49002,0,2],[49003,0,2],[49004,0,2],[49005,0,2],[49006,0,2],[49007,0,2],[49008,0,2],[49009,0,2],[49010,0,2],[49011,0,2],[49012,0,2],[49013,0,2],[49014,0,2],[49015,0,2],[49016,0,1],[49017,0,2],[49018,0,2],[49019,0,2],[49020,0,2],[49021,0,2],[49022,0,2],[49023,0,2],[49024,0,2],[49025,0,2],[49026,0,2],[49027,0,2],[49028,0,2],[49029,0,2],[49030,0,2],[49031,0,2],[49032,0,2],[49033,0,2],[49034,0,2],[49035,0,2],[49036,0,2],[49037,0,2],[49038,0,2],[49039,0,2],[49040,0,2],[49041,0,2],[49042,0,2],[49043,0,2],[49044,0,1],[49045,0,2],[49046,0,2],[49047,0,2],[49048,0,2],[49049,0,2],[49050,0,2],[49051,0,2],[49052,0,2],[49053,0,2],[49054,0,2],[49055,0,2],[49056,0,2],[49057,0,2],[49058,0,2],[49059,0,2],[49060,0,2],[49061,0,2],[49062,0,2],[49063,0,2],[49064,0,2],[49065,0,2],[49066,0,2],[49067,0,2],[49068,0,2],[49069,0,2],[49070,0,2],[49071,0,2],[49072,0,1],[49073,0,2],[49074,0,2],[49075,0,2],[49076,0,2],[49077,0,2],[49078,0,2],[49079,0,2],[49080,0,2],[49081,0,2],[49082,0,2],[49083,0,2],[49084,0,2],[49085,0,2],[49086,0,2],[49087,0,2],[49088,0,2],[49089,0,2],[49090,0,2],[49091,0,2],[49092,0,2],[49093,0,2],[49094,0,2],[49095,0,2],[49096,0,2],[49097,0,2],[49098,0,2],[49099,0,2],[49100,0,1],[49101,0,2],[49102,0,2],[49103,0,2],[49104,0,2],[49105,0,2],[49106,0,2],[49107,0,2],[49108,0,2],[49109,0,2],[49110,0,2],[49111,0,2],[49112,0,2],[49113,0,2],[49114,0,2],[49115,0,2],[49116,0,2],[49117,0,2],[49118,0,2],[49119,0,2],[49120,0,2],[49121,0,2],[49122,0,2],[49123,0,2],[49124,0,2],[49125,0,2],[49126,0,2],[49127,0,2],[49128,0,1],[49129,0,2],[49130,0,2],[49131,0,2],[49132,0,2],[49133,0,2],[49134,0,2],[49135,0,2],[49136,0,2],[49137,0,2],[49138,0,2],[49139,0,2],[49140,0,2],[49141,0,2],[49142,0,2],[49143,0,2],[49144,0,2],[49145,0,2],[49146,0,2],[49147,0,2],[49148,0,2],[49149,0,2],[49150,0,2],[49151,0,2],[49152,0,2],[49153,0,2],[49154,0,2],[49155,0,2],[49156,0,1],[49157,0,2],[49158,0,2],[49159,0,2],[49160,0,2],[49161,0,2],[49162,0,2],[49163,0,2],[49164,0,2],[49165,0,2],[49166,0,2],[49167,0,2],[49168,0,2],[49169,0,2],[49170,0,2],[49171,0,2],[49172,0,2],[49173,0,2],[49174,0,2],[49175,0,2],[49176,0,2],[49177,0,2],[49178,0,2],[49179,0,2],[49180,0,2],[49181,0,2],[49182,0,2],[49183,0,2],[49184,0,1],[49185,0,2],[49186,0,2],[49187,0,2],[49188,0,2],[49189,0,2],[49190,0,2],[49191,0,2],[49192,0,2],[49193,0,2],[49194,0,2],[49195,0,2],[49196,0,2],[49197,0,2],[49198,0,2],[49199,0,2],[49200,0,2],[49201,0,2],[49202,0,2],[49203,0,2],[49204,0,2],[49205,0,2],[49206,0,2],[49207,0,2],[49208,0,2],[49209,0,2],[49210,0,2],[49211,0,2],[49212,0,1],[49213,0,2],[49214,0,2],[49215,0,2],[49216,0,2],[49217,0,2],[49218,0,2],[49219,0,2],[49220,0,2],[49221,0,2],[49222,0,2],[49223,0,2],[49224,0,2],[49225,0,2],[49226,0,2],[49227,0,2],[49228,0,2],[49229,0,2],[49230,0,2],[49231,0,2],[49232,0,2],[49233,0,2],[49234,0,2],[49235,0,2],[49236,0,2],[49237,0,2],[49238,0,2],[49239,0,2],[49240,0,1],[49241,0,2],[49242,0,2],[49243,0,2],[49244,0,2],[49245,0,2],[49246,0,2],[49247,0,2],[49248,0,2],[49249,0,2],[49250,0,2],[49251,0,2],[49252,0,2],[49253,0,2],[49254,0,2],[49255,0,2],[49256,0,2],[49257,0,2],[49258,0,2],[49259,0,2],[49260,0,2],[49261,0,2],[49262,0,2],[49263,0,2],[49264,0,2],[49265,0,2],[49266,0,2],[49267,0,2],[49268,0,1],[49269,0,2],[49270,0,2],[49271,0,2],[49272,0,2],[49273,0,2],[49274,0,2],[49275,0,2],[49276,0,2],[49277,0,2],[49278,0,2],[49279,0,2],[49280,0,2],[49281,0,2],[49282,0,2],[49283,0,2],[49284,0,2],[49285,0,2],[49286,0,2],[49287,0,2],[49288,0,2],[49289,0,2],[49290,0,2],[49291,0,2],[49292,0,2],[49293,0,2],[49294,0,2],[49295,0,2],[49296,0,1],[49297,0,2],[49298,0,2],[49299,0,2],[49300,0,2],[49301,0,2],[49302,0,2],[49303,0,2],[49304,0,2],[49305,0,2],[49306,0,2],[49307,0,2],[49308,0,2],[49309,0,2],[49310,0,2],[49311,0,2],[49312,0,2],[49313,0,2],[49314,0,2],[49315,0,2],[49316,0,2],[49317,0,2],[49318,0,2],[49319,0,2],[49320,0,2],[49321,0,2],[49322,0,2],[49323,0,2],[49324,0,1],[49325,0,2],[49326,0,2],[49327,0,2],[49328,0,2],[49329,0,2],[49330,0,2],[49331,0,2],[49332,0,2],[49333,0,2],[49334,0,2],[49335,0,2],[49336,0,2],[49337,0,2],[49338,0,2],[49339,0,2],[49340,0,2],[49341,0,2],[49342,0,2],[49343,0,2],[49344,0,2],[49345,0,2],[49346,0,2],[49347,0,2],[49348,0,2],[49349,0,2],[49350,0,2],[49351,0,2],[49352,0,1],[49353,0,2],[49354,0,2],[49355,0,2],[49356,0,2],[49357,0,2],[49358,0,2],[49359,0,2],[49360,0,2],[49361,0,2],[49362,0,2],[49363,0,2],[49364,0,2],[49365,0,2],[49366,0,2],[49367,0,2],[49368,0,2],[49369,0,2],[49370,0,2],[49371,0,2],[49372,0,2],[49373,0,2],[49374,0,2],[49375,0,2],[49376,0,2],[49377,0,2],[49378,0,2],[49379,0,2],[49380,0,1],[49381,0,2],[49382,0,2],[49383,0,2],[49384,0,2],[49385,0,2],[49386,0,2],[49387,0,2],[49388,0,2],[49389,0,2],[49390,0,2],[49391,0,2],[49392,0,2],[49393,0,2],[49394,0,2],[49395,0,2],[49396,0,2],[49397,0,2],[49398,0,2],[49399,0,2],[49400,0,2],[49401,0,2],[49402,0,2],[49403,0,2],[49404,0,2],[49405,0,2],[49406,0,2],[49407,0,2],[49408,0,1],[49409,0,2],[49410,0,2],[49411,0,2],[49412,0,2],[49413,0,2],[49414,0,2],[49415,0,2],[49416,0,2],[49417,0,2],[49418,0,2],[49419,0,2],[49420,0,2],[49421,0,2],[49422,0,2],[49423,0,2],[49424,0,2],[49425,0,2],[49426,0,2],[49427,0,2],[49428,0,2],[49429,0,2],[49430,0,2],[49431,0,2],[49432,0,2],[49433,0,2],[49434,0,2],[49435,0,2],[49436,0,1],[49437,0,2],[49438,0,2],[49439,0,2],[49440,0,2],[49441,0,2],[49442,0,2],[49443,0,2],[49444,0,2],[49445,0,2],[49446,0,2],[49447,0,2],[49448,0,2],[49449,0,2],[49450,0,2],[49451,0,2],[49452,0,2],[49453,0,2],[49454,0,2],[49455,0,2],[49456,0,2],[49457,0,2],[49458,0,2],[49459,0,2],[49460,0,2],[49461,0,2],[49462,0,2],[49463,0,2],[49464,0,1],[49465,0,2],[49466,0,2],[49467,0,2],[49468,0,2],[49469,0,2],[49470,0,2],[49471,0,2],[49472,0,2],[49473,0,2],[49474,0,2],[49475,0,2],[49476,0,2],[49477,0,2],[49478,0,2],[49479,0,2],[49480,0,2],[49481,0,2],[49482,0,2],[49483,0,2],[49484,0,2],[49485,0,2],[49486,0,2],[49487,0,2],[49488,0,2],[49489,0,2],[49490,0,2],[49491,0,2],[49492,0,1],[49493,0,2],[49494,0,2],[49495,0,2],[49496,0,2],[49497,0,2],[49498,0,2],[49499,0,2],[49500,0,2],[49501,0,2],[49502,0,2],[49503,0,2],[49504,0,2],[49505,0,2],[49506,0,2],[49507,0,2],[49508,0,2],[49509,0,2],[49510,0,2],[49511,0,2],[49512,0,2],[49513,0,2],[49514,0,2],[49515,0,2],[49516,0,2],[49517,0,2],[49518,0,2],[49519,0,2],[49520,0,1],[49521,0,2],[49522,0,2],[49523,0,2],[49524,0,2],[49525,0,2],[49526,0,2],[49527,0,2],[49528,0,2],[49529,0,2],[49530,0,2],[49531,0,2],[49532,0,2],[49533,0,2],[49534,0,2],[49535,0,2],[49536,0,2],[49537,0,2],[49538,0,2],[49539,0,2],[49540,0,2],[49541,0,2],[49542,0,2],[49543,0,2],[49544,0,2],[49545,0,2],[49546,0,2],[49547,0,2],[49548,0,1],[49549,0,2],[49550,0,2],[49551,0,2],[49552,0,2],[49553,0,2],[49554,0,2],[49555,0,2],[49556,0,2],[49557,0,2],[49558,0,2],[49559,0,2],[49560,0,2],[49561,0,2],[49562,0,2],[49563,0,2],[49564,0,2],[49565,0,2],[49566,0,2],[49567,0,2],[49568,0,2],[49569,0,2],[49570,0,2],[49571,0,2],[49572,0,2],[49573,0,2],[49574,0,2],[49575,0,2],[49576,0,1],[49577,0,2],[49578,0,2],[49579,0,2],[49580,0,2],[49581,0,2],[49582,0,2],[49583,0,2],[49584,0,2],[49585,0,2],[49586,0,2],[49587,0,2],[49588,0,2],[49589,0,2],[49590,0,2],[49591,0,2],[49592,0,2],[49593,0,2],[49594,0,2],[49595,0,2],[49596,0,2],[49597,0,2],[49598,0,2],[49599,0,2],[49600,0,2],[49601,0,2],[49602,0,2],[49603,0,2],[49604,0,1],[49605,0,2],[49606,0,2],[49607,0,2],[49608,0,2],[49609,0,2],[49610,0,2],[49611,0,2],[49612,0,2],[49613,0,2],[49614,0,2],[49615,0,2],[49616,0,2],[49617,0,2],[49618,0,2],[49619,0,2],[49620,0,2],[49621,0,2],[49622,0,2],[49623,0,2],[49624,0,2],[49625,0,2],[49626,0,2],[49627,0,2],[49628,0,2],[49629,0,2],[49630,0,2],[49631,0,2],[49632,0,1],[49633,0,2],[49634,0,2],[49635,0,2],[49636,0,2],[49637,0,2],[49638,0,2],[49639,0,2],[49640,0,2],[49641,0,2],[49642,0,2],[49643,0,2],[49644,0,2],[49645,0,2],[49646,0,2],[49647,0,2],[49648,0,2],[49649,0,2],[49650,0,2],[49651,0,2],[49652,0,2],[49653,0,2],[49654,0,2],[49655,0,2],[49656,0,2],[49657,0,2],[49658,0,2],[49659,0,2],[49660,0,1],[49661,0,2],[49662,0,2],[49663,0,2],[49664,0,2],[49665,0,2],[49666,0,2],[49667,0,2],[49668,0,2],[49669,0,2],[49670,0,2],[49671,0,2],[49672,0,2],[49673,0,2],[49674,0,2],[49675,0,2],[49676,0,2],[49677,0,2],[49678,0,2],[49679,0,2],[49680,0,2],[49681,0,2],[49682,0,2],[49683,0,2],[49684,0,2],[49685,0,2],[49686,0,2],[49687,0,2],[49688,0,1],[49689,0,2],[49690,0,2],[49691,0,2],[49692,0,2],[49693,0,2],[49694,0,2],[49695,0,2],[49696,0,2],[49697,0,2],[49698,0,2],[49699,0,2],[49700,0,2],[49701,0,2],[49702,0,2],[49703,0,2],[49704,0,2],[49705,0,2],[49706,0,2],[49707,0,2],[49708,0,2],[49709,0,2],[49710,0,2],[49711,0,2],[49712,0,2],[49713,0,2],[49714,0,2],[49715,0,2],[49716,0,1],[49717,0,2],[49718,0,2],[49719,0,2],[49720,0,2],[49721,0,2],[49722,0,2],[49723,0,2],[49724,0,2],[49725,0,2],[49726,0,2],[49727,0,2],[49728,0,2],[49729,0,2],[49730,0,2],[49731,0,2],[49732,0,2],[49733,0,2],[49734,0,2],[49735,0,2],[49736,0,2],[49737,0,2],[49738,0,2],[49739,0,2],[49740,0,2],[49741,0,2],[49742,0,2],[49743,0,2],[49744,0,1],[49745,0,2],[49746,0,2],[49747,0,2],[49748,0,2],[49749,0,2],[49750,0,2],[49751,0,2],[49752,0,2],[49753,0,2],[49754,0,2],[49755,0,2],[49756,0,2],[49757,0,2],[49758,0,2],[49759,0,2],[49760,0,2],[49761,0,2],[49762,0,2],[49763,0,2],[49764,0,2],[49765,0,2],[49766,0,2],[49767,0,2],[49768,0,2],[49769,0,2],[49770,0,2],[49771,0,2],[49772,0,1],[49773,0,2],[49774,0,2],[49775,0,2],[49776,0,2],[49777,0,2],[49778,0,2],[49779,0,2],[49780,0,2],[49781,0,2],[49782,0,2],[49783,0,2],[49784,0,2],[49785,0,2],[49786,0,2],[49787,0,2],[49788,0,2],[49789,0,2],[49790,0,2],[49791,0,2],[49792,0,2],[49793,0,2],[49794,0,2],[49795,0,2],[49796,0,2],[49797,0,2],[49798,0,2],[49799,0,2],[49800,0,1],[49801,0,2],[49802,0,2],[49803,0,2],[49804,0,2],[49805,0,2],[49806,0,2],[49807,0,2],[49808,0,2],[49809,0,2],[49810,0,2],[49811,0,2],[49812,0,2],[49813,0,2],[49814,0,2],[49815,0,2],[49816,0,2],[49817,0,2],[49818,0,2],[49819,0,2],[49820,0,2],[49821,0,2],[49822,0,2],[49823,0,2],[49824,0,2],[49825,0,2],[49826,0,2],[49827,0,2],[49828,0,1],[49829,0,2],[49830,0,2],[49831,0,2],[49832,0,2],[49833,0,2],[49834,0,2],[49835,0,2],[49836,0,2],[49837,0,2],[49838,0,2],[49839,0,2],[49840,0,2],[49841,0,2],[49842,0,2],[49843,0,2],[49844,0,2],[49845,0,2],[49846,0,2],[49847,0,2],[49848,0,2],[49849,0,2],[49850,0,2],[49851,0,2],[49852,0,2],[49853,0,2],[49854,0,2],[49855,0,2],[49856,0,1],[49857,0,2],[49858,0,2],[49859,0,2],[49860,0,2],[49861,0,2],[49862,0,2],[49863,0,2],[49864,0,2],[49865,0,2],[49866,0,2],[49867,0,2],[49868,0,2],[49869,0,2],[49870,0,2],[49871,0,2],[49872,0,2],[49873,0,2],[49874,0,2],[49875,0,2],[49876,0,2],[49877,0,2],[49878,0,2],[49879,0,2],[49880,0,2],[49881,0,2],[49882,0,2],[49883,0,2],[49884,0,1],[49885,0,2],[49886,0,2],[49887,0,2],[49888,0,2],[49889,0,2],[49890,0,2],[49891,0,2],[49892,0,2],[49893,0,2],[49894,0,2],[49895,0,2],[49896,0,2],[49897,0,2],[49898,0,2],[49899,0,2],[49900,0,2],[49901,0,2],[49902,0,2],[49903,0,2],[49904,0,2],[49905,0,2],[49906,0,2],[49907,0,2],[49908,0,2],[49909,0,2],[49910,0,2],[49911,0,2],[49912,0,1],[49913,0,2],[49914,0,2],[49915,0,2],[49916,0,2],[49917,0,2],[49918,0,2],[49919,0,2],[49920,0,2],[49921,0,2],[49922,0,2],[49923,0,2],[49924,0,2],[49925,0,2],[49926,0,2],[49927,0,2],[49928,0,2],[49929,0,2],[49930,0,2],[49931,0,2],[49932,0,2],[49933,0,2],[49934,0,2],[49935,0,2],[49936,0,2],[49937,0,2],[49938,0,2],[49939,0,2],[49940,0,1],[49941,0,2],[49942,0,2],[49943,0,2],[49944,0,2],[49945,0,2],[49946,0,2],[49947,0,2],[49948,0,2],[49949,0,2],[49950,0,2],[49951,0,2],[49952,0,2],[49953,0,2],[49954,0,2],[49955,0,2],[49956,0,2],[49957,0,2],[49958,0,2],[49959,0,2],[49960,0,2],[49961,0,2],[49962,0,2],[49963,0,2],[49964,0,2],[49965,0,2],[49966,0,2],[49967,0,2],[49968,0,1],[49969,0,2],[49970,0,2],[49971,0,2],[49972,0,2],[49973,0,2],[49974,0,2],[49975,0,2],[49976,0,2],[49977,0,2],[49978,0,2],[49979,0,2],[49980,0,2],[49981,0,2],[49982,0,2],[49983,0,2],[49984,0,2],[49985,0,2],[49986,0,2],[49987,0,2],[49988,0,2],[49989,0,2],[49990,0,2],[49991,0,2],[49992,0,2],[49993,0,2],[49994,0,2],[49995,0,2],[49996,0,1],[49997,0,2],[49998,0,2],[49999,0,2],[50000,0,2],[50001,0,2],[50002,0,2],[50003,0,2],[50004,0,2],[50005,0,2],[50006,0,2],[50007,0,2],[50008,0,2],[50009,0,2],[50010,0,2],[50011,0,2],[50012,0,2],[50013,0,2],[50014,0,2],[50015,0,2],[50016,0,2],[50017,0,2],[50018,0,2],[50019,0,2],[50020,0,2],[50021,0,2],[50022,0,2],[50023,0,2],[50024,0,1],[50025,0,2],[50026,0,2],[50027,0,2],[50028,0,2],[50029,0,2],[50030,0,2],[50031,0,2],[50032,0,2],[50033,0,2],[50034,0,2],[50035,0,2],[50036,0,2],[50037,0,2],[50038,0,2],[50039,0,2],[50040,0,2],[50041,0,2],[50042,0,2],[50043,0,2],[50044,0,2],[50045,0,2],[50046,0,2],[50047,0,2],[50048,0,2],[50049,0,2],[50050,0,2],[50051,0,2],[50052,0,1],[50053,0,2],[50054,0,2],[50055,0,2],[50056,0,2],[50057,0,2],[50058,0,2],[50059,0,2],[50060,0,2],[50061,0,2],[50062,0,2],[50063,0,2],[50064,0,2],[50065,0,2],[50066,0,2],[50067,0,2],[50068,0,2],[50069,0,2],[50070,0,2],[50071,0,2],[50072,0,2],[50073,0,2],[50074,0,2],[50075,0,2],[50076,0,2],[50077,0,2],[50078,0,2],[50079,0,2],[50080,0,1],[50081,0,2],[50082,0,2],[50083,0,2],[50084,0,2],[50085,0,2],[50086,0,2],[50087,0,2],[50088,0,2],[50089,0,2],[50090,0,2],[50091,0,2],[50092,0,2],[50093,0,2],[50094,0,2],[50095,0,2],[50096,0,2],[50097,0,2],[50098,0,2],[50099,0,2],[50100,0,2],[50101,0,2],[50102,0,2],[50103,0,2],[50104,0,2],[50105,0,2],[50106,0,2],[50107,0,2],[50108,0,1],[50109,0,2],[50110,0,2],[50111,0,2],[50112,0,2],[50113,0,2],[50114,0,2],[50115,0,2],[50116,0,2],[50117,0,2],[50118,0,2],[50119,0,2],[50120,0,2],[50121,0,2],[50122,0,2],[50123,0,2],[50124,0,2],[50125,0,2],[50126,0,2],[50127,0,2],[50128,0,2],[50129,0,2],[50130,0,2],[50131,0,2],[50132,0,2],[50133,0,2],[50134,0,2],[50135,0,2],[50136,0,1],[50137,0,2],[50138,0,2],[50139,0,2],[50140,0,2],[50141,0,2],[50142,0,2],[50143,0,2],[50144,0,2],[50145,0,2],[50146,0,2],[50147,0,2],[50148,0,2],[50149,0,2],[50150,0,2],[50151,0,2],[50152,0,2],[50153,0,2],[50154,0,2],[50155,0,2],[50156,0,2],[50157,0,2],[50158,0,2],[50159,0,2],[50160,0,2],[50161,0,2],[50162,0,2],[50163,0,2],[50164,0,1],[50165,0,2],[50166,0,2],[50167,0,2],[50168,0,2],[50169,0,2],[50170,0,2],[50171,0,2],[50172,0,2],[50173,0,2],[50174,0,2],[50175,0,2],[50176,0,2],[50177,0,2],[50178,0,2],[50179,0,2],[50180,0,2],[50181,0,2],[50182,0,2],[50183,0,2],[50184,0,2],[50185,0,2],[50186,0,2],[50187,0,2],[50188,0,2],[50189,0,2],[50190,0,2],[50191,0,2],[50192,0,1],[50193,0,2],[50194,0,2],[50195,0,2],[50196,0,2],[50197,0,2],[50198,0,2],[50199,0,2],[50200,0,2],[50201,0,2],[50202,0,2],[50203,0,2],[50204,0,2],[50205,0,2],[50206,0,2],[50207,0,2],[50208,0,2],[50209,0,2],[50210,0,2],[50211,0,2],[50212,0,2],[50213,0,2],[50214,0,2],[50215,0,2],[50216,0,2],[50217,0,2],[50218,0,2],[50219,0,2],[50220,0,1],[50221,0,2],[50222,0,2],[50223,0,2],[50224,0,2],[50225,0,2],[50226,0,2],[50227,0,2],[50228,0,2],[50229,0,2],[50230,0,2],[50231,0,2],[50232,0,2],[50233,0,2],[50234,0,2],[50235,0,2],[50236,0,2],[50237,0,2],[50238,0,2],[50239,0,2],[50240,0,2],[50241,0,2],[50242,0,2],[50243,0,2],[50244,0,2],[50245,0,2],[50246,0,2],[50247,0,2],[50248,0,1],[50249,0,2],[50250,0,2],[50251,0,2],[50252,0,2],[50253,0,2],[50254,0,2],[50255,0,2],[50256,0,2],[50257,0,2],[50258,0,2],[50259,0,2],[50260,0,2],[50261,0,2],[50262,0,2],[50263,0,2],[50264,0,2],[50265,0,2],[50266,0,2],[50267,0,2],[50268,0,2],[50269,0,2],[50270,0,2],[50271,0,2],[50272,0,2],[50273,0,2],[50274,0,2],[50275,0,2],[50276,0,1],[50277,0,2],[50278,0,2],[50279,0,2],[50280,0,2],[50281,0,2],[50282,0,2],[50283,0,2],[50284,0,2],[50285,0,2],[50286,0,2],[50287,0,2],[50288,0,2],[50289,0,2],[50290,0,2],[50291,0,2],[50292,0,2],[50293,0,2],[50294,0,2],[50295,0,2],[50296,0,2],[50297,0,2],[50298,0,2],[50299,0,2],[50300,0,2],[50301,0,2],[50302,0,2],[50303,0,2],[50304,0,1],[50305,0,2],[50306,0,2],[50307,0,2],[50308,0,2],[50309,0,2],[50310,0,2],[50311,0,2],[50312,0,2],[50313,0,2],[50314,0,2],[50315,0,2],[50316,0,2],[50317,0,2],[50318,0,2],[50319,0,2],[50320,0,2],[50321,0,2],[50322,0,2],[50323,0,2],[50324,0,2],[50325,0,2],[50326,0,2],[50327,0,2],[50328,0,2],[50329,0,2],[50330,0,2],[50331,0,2],[50332,0,1],[50333,0,2],[50334,0,2],[50335,0,2],[50336,0,2],[50337,0,2],[50338,0,2],[50339,0,2],[50340,0,2],[50341,0,2],[50342,0,2],[50343,0,2],[50344,0,2],[50345,0,2],[50346,0,2],[50347,0,2],[50348,0,2],[50349,0,2],[50350,0,2],[50351,0,2],[50352,0,2],[50353,0,2],[50354,0,2],[50355,0,2],[50356,0,2],[50357,0,2],[50358,0,2],[50359,0,2],[50360,0,1],[50361,0,2],[50362,0,2],[50363,0,2],[50364,0,2],[50365,0,2],[50366,0,2],[50367,0,2],[50368,0,2],[50369,0,2],[50370,0,2],[50371,0,2],[50372,0,2],[50373,0,2],[50374,0,2],[50375,0,2],[50376,0,2],[50377,0,2],[50378,0,2],[50379,0,2],[50380,0,2],[50381,0,2],[50382,0,2],[50383,0,2],[50384,0,2],[50385,0,2],[50386,0,2],[50387,0,2],[50388,0,1],[50389,0,2],[50390,0,2],[50391,0,2],[50392,0,2],[50393,0,2],[50394,0,2],[50395,0,2],[50396,0,2],[50397,0,2],[50398,0,2],[50399,0,2],[50400,0,2],[50401,0,2],[50402,0,2],[50403,0,2],[50404,0,2],[50405,0,2],[50406,0,2],[50407,0,2],[50408,0,2],[50409,0,2],[50410,0,2],[50411,0,2],[50412,0,2],[50413,0,2],[50414,0,2],[50415,0,2],[50416,0,1],[50417,0,2],[50418,0,2],[50419,0,2],[50420,0,2],[50421,0,2],[50422,0,2],[50423,0,2],[50424,0,2],[50425,0,2],[50426,0,2],[50427,0,2],[50428,0,2],[50429,0,2],[50430,0,2],[50431,0,2],[50432,0,2],[50433,0,2],[50434,0,2],[50435,0,2],[50436,0,2],[50437,0,2],[50438,0,2],[50439,0,2],[50440,0,2],[50441,0,2],[50442,0,2],[50443,0,2],[50444,0,1],[50445,0,2],[50446,0,2],[50447,0,2],[50448,0,2],[50449,0,2],[50450,0,2],[50451,0,2],[50452,0,2],[50453,0,2],[50454,0,2],[50455,0,2],[50456,0,2],[50457,0,2],[50458,0,2],[50459,0,2],[50460,0,2],[50461,0,2],[50462,0,2],[50463,0,2],[50464,0,2],[50465,0,2],[50466,0,2],[50467,0,2],[50468,0,2],[50469,0,2],[50470,0,2],[50471,0,2],[50472,0,1],[50473,0,2],[50474,0,2],[50475,0,2],[50476,0,2],[50477,0,2],[50478,0,2],[50479,0,2],[50480,0,2],[50481,0,2],[50482,0,2],[50483,0,2],[50484,0,2],[50485,0,2],[50486,0,2],[50487,0,2],[50488,0,2],[50489,0,2],[50490,0,2],[50491,0,2],[50492,0,2],[50493,0,2],[50494,0,2],[50495,0,2],[50496,0,2],[50497,0,2],[50498,0,2],[50499,0,2],[50500,0,1],[50501,0,2],[50502,0,2],[50503,0,2],[50504,0,2],[50505,0,2],[50506,0,2],[50507,0,2],[50508,0,2],[50509,0,2],[50510,0,2],[50511,0,2],[50512,0,2],[50513,0,2],[50514,0,2],[50515,0,2],[50516,0,2],[50517,0,2],[50518,0,2],[50519,0,2],[50520,0,2],[50521,0,2],[50522,0,2],[50523,0,2],[50524,0,2],[50525,0,2],[50526,0,2],[50527,0,2],[50528,0,1],[50529,0,2],[50530,0,2],[50531,0,2],[50532,0,2],[50533,0,2],[50534,0,2],[50535,0,2],[50536,0,2],[50537,0,2],[50538,0,2],[50539,0,2],[50540,0,2],[50541,0,2],[50542,0,2],[50543,0,2],[50544,0,2],[50545,0,2],[50546,0,2],[50547,0,2],[50548,0,2],[50549,0,2],[50550,0,2],[50551,0,2],[50552,0,2],[50553,0,2],[50554,0,2],[50555,0,2],[50556,0,1],[50557,0,2],[50558,0,2],[50559,0,2],[50560,0,2],[50561,0,2],[50562,0,2],[50563,0,2],[50564,0,2],[50565,0,2],[50566,0,2],[50567,0,2],[50568,0,2],[50569,0,2],[50570,0,2],[50571,0,2],[50572,0,2],[50573,0,2],[50574,0,2],[50575,0,2],[50576,0,2],[50577,0,2],[50578,0,2],[50579,0,2],[50580,0,2],[50581,0,2],[50582,0,2],[50583,0,2],[50584,0,1],[50585,0,2],[50586,0,2],[50587,0,2],[50588,0,2],[50589,0,2],[50590,0,2],[50591,0,2],[50592,0,2],[50593,0,2],[50594,0,2],[50595,0,2],[50596,0,2],[50597,0,2],[50598,0,2],[50599,0,2],[50600,0,2],[50601,0,2],[50602,0,2],[50603,0,2],[50604,0,2],[50605,0,2],[50606,0,2],[50607,0,2],[50608,0,2],[50609,0,2],[50610,0,2],[50611,0,2],[50612,0,1],[50613,0,2],[50614,0,2],[50615,0,2],[50616,0,2],[50617,0,2],[50618,0,2],[50619,0,2],[50620,0,2],[50621,0,2],[50622,0,2],[50623,0,2],[50624,0,2],[50625,0,2],[50626,0,2],[50627,0,2],[50628,0,2],[50629,0,2],[50630,0,2],[50631,0,2],[50632,0,2],[50633,0,2],[50634,0,2],[50635,0,2],[50636,0,2],[50637,0,2],[50638,0,2],[50639,0,2],[50640,0,1],[50641,0,2],[50642,0,2],[50643,0,2],[50644,0,2],[50645,0,2],[50646,0,2],[50647,0,2],[50648,0,2],[50649,0,2],[50650,0,2],[50651,0,2],[50652,0,2],[50653,0,2],[50654,0,2],[50655,0,2],[50656,0,2],[50657,0,2],[50658,0,2],[50659,0,2],[50660,0,2],[50661,0,2],[50662,0,2],[50663,0,2],[50664,0,2],[50665,0,2],[50666,0,2],[50667,0,2],[50668,0,1],[50669,0,2],[50670,0,2],[50671,0,2],[50672,0,2],[50673,0,2],[50674,0,2],[50675,0,2],[50676,0,2],[50677,0,2],[50678,0,2],[50679,0,2],[50680,0,2],[50681,0,2],[50682,0,2],[50683,0,2],[50684,0,2],[50685,0,2],[50686,0,2],[50687,0,2],[50688,0,2],[50689,0,2],[50690,0,2],[50691,0,2],[50692,0,2],[50693,0,2],[50694,0,2],[50695,0,2],[50696,0,1],[50697,0,2],[50698,0,2],[50699,0,2],[50700,0,2],[50701,0,2],[50702,0,2],[50703,0,2],[50704,0,2],[50705,0,2],[50706,0,2],[50707,0,2],[50708,0,2],[50709,0,2],[50710,0,2],[50711,0,2],[50712,0,2],[50713,0,2],[50714,0,2],[50715,0,2],[50716,0,2],[50717,0,2],[50718,0,2],[50719,0,2],[50720,0,2],[50721,0,2],[50722,0,2],[50723,0,2],[50724,0,1],[50725,0,2],[50726,0,2],[50727,0,2],[50728,0,2],[50729,0,2],[50730,0,2],[50731,0,2],[50732,0,2],[50733,0,2],[50734,0,2],[50735,0,2],[50736,0,2],[50737,0,2],[50738,0,2],[50739,0,2],[50740,0,2],[50741,0,2],[50742,0,2],[50743,0,2],[50744,0,2],[50745,0,2],[50746,0,2],[50747,0,2],[50748,0,2],[50749,0,2],[50750,0,2],[50751,0,2],[50752,0,1],[50753,0,2],[50754,0,2],[50755,0,2],[50756,0,2],[50757,0,2],[50758,0,2],[50759,0,2],[50760,0,2],[50761,0,2],[50762,0,2],[50763,0,2],[50764,0,2],[50765,0,2],[50766,0,2],[50767,0,2],[50768,0,2],[50769,0,2],[50770,0,2],[50771,0,2],[50772,0,2],[50773,0,2],[50774,0,2],[50775,0,2],[50776,0,2],[50777,0,2],[50778,0,2],[50779,0,2],[50780,0,1],[50781,0,2],[50782,0,2],[50783,0,2],[50784,0,2],[50785,0,2],[50786,0,2],[50787,0,2],[50788,0,2],[50789,0,2],[50790,0,2],[50791,0,2],[50792,0,2],[50793,0,2],[50794,0,2],[50795,0,2],[50796,0,2],[50797,0,2],[50798,0,2],[50799,0,2],[50800,0,2],[50801,0,2],[50802,0,2],[50803,0,2],[50804,0,2],[50805,0,2],[50806,0,2],[50807,0,2],[50808,0,1],[50809,0,2],[50810,0,2],[50811,0,2],[50812,0,2],[50813,0,2],[50814,0,2],[50815,0,2],[50816,0,2],[50817,0,2],[50818,0,2],[50819,0,2],[50820,0,2],[50821,0,2],[50822,0,2],[50823,0,2],[50824,0,2],[50825,0,2],[50826,0,2],[50827,0,2],[50828,0,2],[50829,0,2],[50830,0,2],[50831,0,2],[50832,0,2],[50833,0,2],[50834,0,2],[50835,0,2],[50836,0,1],[50837,0,2],[50838,0,2],[50839,0,2],[50840,0,2],[50841,0,2],[50842,0,2],[50843,0,2],[50844,0,2],[50845,0,2],[50846,0,2],[50847,0,2],[50848,0,2],[50849,0,2],[50850,0,2],[50851,0,2],[50852,0,2],[50853,0,2],[50854,0,2],[50855,0,2],[50856,0,2],[50857,0,2],[50858,0,2],[50859,0,2],[50860,0,2],[50861,0,2],[50862,0,2],[50863,0,2],[50864,0,1],[50865,0,2],[50866,0,2],[50867,0,2],[50868,0,2],[50869,0,2],[50870,0,2],[50871,0,2],[50872,0,2],[50873,0,2],[50874,0,2],[50875,0,2],[50876,0,2],[50877,0,2],[50878,0,2],[50879,0,2],[50880,0,2],[50881,0,2],[50882,0,2],[50883,0,2],[50884,0,2],[50885,0,2],[50886,0,2],[50887,0,2],[50888,0,2],[50889,0,2],[50890,0,2],[50891,0,2],[50892,0,1],[50893,0,2],[50894,0,2],[50895,0,2],[50896,0,2],[50897,0,2],[50898,0,2],[50899,0,2],[50900,0,2],[50901,0,2],[50902,0,2],[50903,0,2],[50904,0,2],[50905,0,2],[50906,0,2],[50907,0,2],[50908,0,2],[50909,0,2],[50910,0,2],[50911,0,2],[50912,0,2],[50913,0,2],[50914,0,2],[50915,0,2],[50916,0,2],[50917,0,2],[50918,0,2],[50919,0,2],[50920,0,1],[50921,0,2],[50922,0,2],[50923,0,2],[50924,0,2],[50925,0,2],[50926,0,2],[50927,0,2],[50928,0,2],[50929,0,2],[50930,0,2],[50931,0,2],[50932,0,2],[50933,0,2],[50934,0,2],[50935,0,2],[50936,0,2],[50937,0,2],[50938,0,2],[50939,0,2],[50940,0,2],[50941,0,2],[50942,0,2],[50943,0,2],[50944,0,2],[50945,0,2],[50946,0,2],[50947,0,2],[50948,0,1],[50949,0,2],[50950,0,2],[50951,0,2],[50952,0,2],[50953,0,2],[50954,0,2],[50955,0,2],[50956,0,2],[50957,0,2],[50958,0,2],[50959,0,2],[50960,0,2],[50961,0,2],[50962,0,2],[50963,0,2],[50964,0,2],[50965,0,2],[50966,0,2],[50967,0,2],[50968,0,2],[50969,0,2],[50970,0,2],[50971,0,2],[50972,0,2],[50973,0,2],[50974,0,2],[50975,0,2],[50976,0,1],[50977,0,2],[50978,0,2],[50979,0,2],[50980,0,2],[50981,0,2],[50982,0,2],[50983,0,2],[50984,0,2],[50985,0,2],[50986,0,2],[50987,0,2],[50988,0,2],[50989,0,2],[50990,0,2],[50991,0,2],[50992,0,2],[50993,0,2],[50994,0,2],[50995,0,2],[50996,0,2],[50997,0,2],[50998,0,2],[50999,0,2],[51000,0,2],[51001,0,2],[51002,0,2],[51003,0,2],[51004,0,1],[51005,0,2],[51006,0,2],[51007,0,2],[51008,0,2],[51009,0,2],[51010,0,2],[51011,0,2],[51012,0,2],[51013,0,2],[51014,0,2],[51015,0,2],[51016,0,2],[51017,0,2],[51018,0,2],[51019,0,2],[51020,0,2],[51021,0,2],[51022,0,2],[51023,0,2],[51024,0,2],[51025,0,2],[51026,0,2],[51027,0,2],[51028,0,2],[51029,0,2],[51030,0,2],[51031,0,2],[51032,0,1],[51033,0,2],[51034,0,2],[51035,0,2],[51036,0,2],[51037,0,2],[51038,0,2],[51039,0,2],[51040,0,2],[51041,0,2],[51042,0,2],[51043,0,2],[51044,0,2],[51045,0,2],[51046,0,2],[51047,0,2],[51048,0,2],[51049,0,2],[51050,0,2],[51051,0,2],[51052,0,2],[51053,0,2],[51054,0,2],[51055,0,2],[51056,0,2],[51057,0,2],[51058,0,2],[51059,0,2],[51060,0,1],[51061,0,2],[51062,0,2],[51063,0,2],[51064,0,2],[51065,0,2],[51066,0,2],[51067,0,2],[51068,0,2],[51069,0,2],[51070,0,2],[51071,0,2],[51072,0,2],[51073,0,2],[51074,0,2],[51075,0,2],[51076,0,2],[51077,0,2],[51078,0,2],[51079,0,2],[51080,0,2],[51081,0,2],[51082,0,2],[51083,0,2],[51084,0,2],[51085,0,2],[51086,0,2],[51087,0,2],[51088,0,1],[51089,0,2],[51090,0,2],[51091,0,2],[51092,0,2],[51093,0,2],[51094,0,2],[51095,0,2],[51096,0,2],[51097,0,2],[51098,0,2],[51099,0,2],[51100,0,2],[51101,0,2],[51102,0,2],[51103,0,2],[51104,0,2],[51105,0,2],[51106,0,2],[51107,0,2],[51108,0,2],[51109,0,2],[51110,0,2],[51111,0,2],[51112,0,2],[51113,0,2],[51114,0,2],[51115,0,2],[51116,0,1],[51117,0,2],[51118,0,2],[51119,0,2],[51120,0,2],[51121,0,2],[51122,0,2],[51123,0,2],[51124,0,2],[51125,0,2],[51126,0,2],[51127,0,2],[51128,0,2],[51129,0,2],[51130,0,2],[51131,0,2],[51132,0,2],[51133,0,2],[51134,0,2],[51135,0,2],[51136,0,2],[51137,0,2],[51138,0,2],[51139,0,2],[51140,0,2],[51141,0,2],[51142,0,2],[51143,0,2],[51144,0,1],[51145,0,2],[51146,0,2],[51147,0,2],[51148,0,2],[51149,0,2],[51150,0,2],[51151,0,2],[51152,0,2],[51153,0,2],[51154,0,2],[51155,0,2],[51156,0,2],[51157,0,2],[51158,0,2],[51159,0,2],[51160,0,2],[51161,0,2],[51162,0,2],[51163,0,2],[51164,0,2],[51165,0,2],[51166,0,2],[51167,0,2],[51168,0,2],[51169,0,2],[51170,0,2],[51171,0,2],[51172,0,1],[51173,0,2],[51174,0,2],[51175,0,2],[51176,0,2],[51177,0,2],[51178,0,2],[51179,0,2],[51180,0,2],[51181,0,2],[51182,0,2],[51183,0,2],[51184,0,2],[51185,0,2],[51186,0,2],[51187,0,2],[51188,0,2],[51189,0,2],[51190,0,2],[51191,0,2],[51192,0,2],[51193,0,2],[51194,0,2],[51195,0,2],[51196,0,2],[51197,0,2],[51198,0,2],[51199,0,2],[51200,0,1],[51201,0,2],[51202,0,2],[51203,0,2],[51204,0,2],[51205,0,2],[51206,0,2],[51207,0,2],[51208,0,2],[51209,0,2],[51210,0,2],[51211,0,2],[51212,0,2],[51213,0,2],[51214,0,2],[51215,0,2],[51216,0,2],[51217,0,2],[51218,0,2],[51219,0,2],[51220,0,2],[51221,0,2],[51222,0,2],[51223,0,2],[51224,0,2],[51225,0,2],[51226,0,2],[51227,0,2],[51228,0,1],[51229,0,2],[51230,0,2],[51231,0,2],[51232,0,2],[51233,0,2],[51234,0,2],[51235,0,2],[51236,0,2],[51237,0,2],[51238,0,2],[51239,0,2],[51240,0,2],[51241,0,2],[51242,0,2],[51243,0,2],[51244,0,2],[51245,0,2],[51246,0,2],[51247,0,2],[51248,0,2],[51249,0,2],[51250,0,2],[51251,0,2],[51252,0,2],[51253,0,2],[51254,0,2],[51255,0,2],[51256,0,1],[51257,0,2],[51258,0,2],[51259,0,2],[51260,0,2],[51261,0,2],[51262,0,2],[51263,0,2],[51264,0,2],[51265,0,2],[51266,0,2],[51267,0,2],[51268,0,2],[51269,0,2],[51270,0,2],[51271,0,2],[51272,0,2],[51273,0,2],[51274,0,2],[51275,0,2],[51276,0,2],[51277,0,2],[51278,0,2],[51279,0,2],[51280,0,2],[51281,0,2],[51282,0,2],[51283,0,2],[51284,0,1],[51285,0,2],[51286,0,2],[51287,0,2],[51288,0,2],[51289,0,2],[51290,0,2],[51291,0,2],[51292,0,2],[51293,0,2],[51294,0,2],[51295,0,2],[51296,0,2],[51297,0,2],[51298,0,2],[51299,0,2],[51300,0,2],[51301,0,2],[51302,0,2],[51303,0,2],[51304,0,2],[51305,0,2],[51306,0,2],[51307,0,2],[51308,0,2],[51309,0,2],[51310,0,2],[51311,0,2],[51312,0,1],[51313,0,2],[51314,0,2],[51315,0,2],[51316,0,2],[51317,0,2],[51318,0,2],[51319,0,2],[51320,0,2],[51321,0,2],[51322,0,2],[51323,0,2],[51324,0,2],[51325,0,2],[51326,0,2],[51327,0,2],[51328,0,2],[51329,0,2],[51330,0,2],[51331,0,2],[51332,0,2],[51333,0,2],[51334,0,2],[51335,0,2],[51336,0,2],[51337,0,2],[51338,0,2],[51339,0,2],[51340,0,1],[51341,0,2],[51342,0,2],[51343,0,2],[51344,0,2],[51345,0,2],[51346,0,2],[51347,0,2],[51348,0,2],[51349,0,2],[51350,0,2],[51351,0,2],[51352,0,2],[51353,0,2],[51354,0,2],[51355,0,2],[51356,0,2],[51357,0,2],[51358,0,2],[51359,0,2],[51360,0,2],[51361,0,2],[51362,0,2],[51363,0,2],[51364,0,2],[51365,0,2],[51366,0,2],[51367,0,2],[51368,0,1],[51369,0,2],[51370,0,2],[51371,0,2],[51372,0,2],[51373,0,2],[51374,0,2],[51375,0,2],[51376,0,2],[51377,0,2],[51378,0,2],[51379,0,2],[51380,0,2],[51381,0,2],[51382,0,2],[51383,0,2],[51384,0,2],[51385,0,2],[51386,0,2],[51387,0,2],[51388,0,2],[51389,0,2],[51390,0,2],[51391,0,2],[51392,0,2],[51393,0,2],[51394,0,2],[51395,0,2],[51396,0,1],[51397,0,2],[51398,0,2],[51399,0,2],[51400,0,2],[51401,0,2],[51402,0,2],[51403,0,2],[51404,0,2],[51405,0,2],[51406,0,2],[51407,0,2],[51408,0,2],[51409,0,2],[51410,0,2],[51411,0,2],[51412,0,2],[51413,0,2],[51414,0,2],[51415,0,2],[51416,0,2],[51417,0,2],[51418,0,2],[51419,0,2],[51420,0,2],[51421,0,2],[51422,0,2],[51423,0,2],[51424,0,1],[51425,0,2],[51426,0,2],[51427,0,2],[51428,0,2],[51429,0,2],[51430,0,2],[51431,0,2],[51432,0,2],[51433,0,2],[51434,0,2],[51435,0,2],[51436,0,2],[51437,0,2],[51438,0,2],[51439,0,2],[51440,0,2],[51441,0,2],[51442,0,2],[51443,0,2],[51444,0,2],[51445,0,2],[51446,0,2],[51447,0,2],[51448,0,2],[51449,0,2],[51450,0,2],[51451,0,2],[51452,0,1],[51453,0,2],[51454,0,2],[51455,0,2],[51456,0,2],[51457,0,2],[51458,0,2],[51459,0,2],[51460,0,2],[51461,0,2],[51462,0,2],[51463,0,2],[51464,0,2],[51465,0,2],[51466,0,2],[51467,0,2],[51468,0,2],[51469,0,2],[51470,0,2],[51471,0,2],[51472,0,2],[51473,0,2],[51474,0,2],[51475,0,2],[51476,0,2],[51477,0,2],[51478,0,2],[51479,0,2],[51480,0,1],[51481,0,2],[51482,0,2],[51483,0,2],[51484,0,2],[51485,0,2],[51486,0,2],[51487,0,2],[51488,0,2],[51489,0,2],[51490,0,2],[51491,0,2],[51492,0,2],[51493,0,2],[51494,0,2],[51495,0,2],[51496,0,2],[51497,0,2],[51498,0,2],[51499,0,2],[51500,0,2],[51501,0,2],[51502,0,2],[51503,0,2],[51504,0,2],[51505,0,2],[51506,0,2],[51507,0,2],[51508,0,1],[51509,0,2],[51510,0,2],[51511,0,2],[51512,0,2],[51513,0,2],[51514,0,2],[51515,0,2],[51516,0,2],[51517,0,2],[51518,0,2],[51519,0,2],[51520,0,2],[51521,0,2],[51522,0,2],[51523,0,2],[51524,0,2],[51525,0,2],[51526,0,2],[51527,0,2],[51528,0,2],[51529,0,2],[51530,0,2],[51531,0,2],[51532,0,2],[51533,0,2],[51534,0,2],[51535,0,2],[51536,0,1],[51537,0,2],[51538,0,2],[51539,0,2],[51540,0,2],[51541,0,2],[51542,0,2],[51543,0,2],[51544,0,2],[51545,0,2],[51546,0,2],[51547,0,2],[51548,0,2],[51549,0,2],[51550,0,2],[51551,0,2],[51552,0,2],[51553,0,2],[51554,0,2],[51555,0,2],[51556,0,2],[51557,0,2],[51558,0,2],[51559,0,2],[51560,0,2],[51561,0,2],[51562,0,2],[51563,0,2],[51564,0,1],[51565,0,2],[51566,0,2],[51567,0,2],[51568,0,2],[51569,0,2],[51570,0,2],[51571,0,2],[51572,0,2],[51573,0,2],[51574,0,2],[51575,0,2],[51576,0,2],[51577,0,2],[51578,0,2],[51579,0,2],[51580,0,2],[51581,0,2],[51582,0,2],[51583,0,2],[51584,0,2],[51585,0,2],[51586,0,2],[51587,0,2],[51588,0,2],[51589,0,2],[51590,0,2],[51591,0,2],[51592,0,1],[51593,0,2],[51594,0,2],[51595,0,2],[51596,0,2],[51597,0,2],[51598,0,2],[51599,0,2],[51600,0,2],[51601,0,2],[51602,0,2],[51603,0,2],[51604,0,2],[51605,0,2],[51606,0,2],[51607,0,2],[51608,0,2],[51609,0,2],[51610,0,2],[51611,0,2],[51612,0,2],[51613,0,2],[51614,0,2],[51615,0,2],[51616,0,2],[51617,0,2],[51618,0,2],[51619,0,2],[51620,0,1],[51621,0,2],[51622,0,2],[51623,0,2],[51624,0,2],[51625,0,2],[51626,0,2],[51627,0,2],[51628,0,2],[51629,0,2],[51630,0,2],[51631,0,2],[51632,0,2],[51633,0,2],[51634,0,2],[51635,0,2],[51636,0,2],[51637,0,2],[51638,0,2],[51639,0,2],[51640,0,2],[51641,0,2],[51642,0,2],[51643,0,2],[51644,0,2],[51645,0,2],[51646,0,2],[51647,0,2],[51648,0,1],[51649,0,2],[51650,0,2],[51651,0,2],[51652,0,2],[51653,0,2],[51654,0,2],[51655,0,2],[51656,0,2],[51657,0,2],[51658,0,2],[51659,0,2],[51660,0,2],[51661,0,2],[51662,0,2],[51663,0,2],[51664,0,2],[51665,0,2],[51666,0,2],[51667,0,2],[51668,0,2],[51669,0,2],[51670,0,2],[51671,0,2],[51672,0,2],[51673,0,2],[51674,0,2],[51675,0,2],[51676,0,1],[51677,0,2],[51678,0,2],[51679,0,2],[51680,0,2],[51681,0,2],[51682,0,2],[51683,0,2],[51684,0,2],[51685,0,2],[51686,0,2],[51687,0,2],[51688,0,2],[51689,0,2],[51690,0,2],[51691,0,2],[51692,0,2],[51693,0,2],[51694,0,2],[51695,0,2],[51696,0,2],[51697,0,2],[51698,0,2],[51699,0,2],[51700,0,2],[51701,0,2],[51702,0,2],[51703,0,2],[51704,0,1],[51705,0,2],[51706,0,2],[51707,0,2],[51708,0,2],[51709,0,2],[51710,0,2],[51711,0,2],[51712,0,2],[51713,0,2],[51714,0,2],[51715,0,2],[51716,0,2],[51717,0,2],[51718,0,2],[51719,0,2],[51720,0,2],[51721,0,2],[51722,0,2],[51723,0,2],[51724,0,2],[51725,0,2],[51726,0,2],[51727,0,2],[51728,0,2],[51729,0,2],[51730,0,2],[51731,0,2],[51732,0,1],[51733,0,2],[51734,0,2],[51735,0,2],[51736,0,2],[51737,0,2],[51738,0,2],[51739,0,2],[51740,0,2],[51741,0,2],[51742,0,2],[51743,0,2],[51744,0,2],[51745,0,2],[51746,0,2],[51747,0,2],[51748,0,2],[51749,0,2],[51750,0,2],[51751,0,2],[51752,0,2],[51753,0,2],[51754,0,2],[51755,0,2],[51756,0,2],[51757,0,2],[51758,0,2],[51759,0,2],[51760,0,1],[51761,0,2],[51762,0,2],[51763,0,2],[51764,0,2],[51765,0,2],[51766,0,2],[51767,0,2],[51768,0,2],[51769,0,2],[51770,0,2],[51771,0,2],[51772,0,2],[51773,0,2],[51774,0,2],[51775,0,2],[51776,0,2],[51777,0,2],[51778,0,2],[51779,0,2],[51780,0,2],[51781,0,2],[51782,0,2],[51783,0,2],[51784,0,2],[51785,0,2],[51786,0,2],[51787,0,2],[51788,0,1],[51789,0,2],[51790,0,2],[51791,0,2],[51792,0,2],[51793,0,2],[51794,0,2],[51795,0,2],[51796,0,2],[51797,0,2],[51798,0,2],[51799,0,2],[51800,0,2],[51801,0,2],[51802,0,2],[51803,0,2],[51804,0,2],[51805,0,2],[51806,0,2],[51807,0,2],[51808,0,2],[51809,0,2],[51810,0,2],[51811,0,2],[51812,0,2],[51813,0,2],[51814,0,2],[51815,0,2],[51816,0,1],[51817,0,2],[51818,0,2],[51819,0,2],[51820,0,2],[51821,0,2],[51822,0,2],[51823,0,2],[51824,0,2],[51825,0,2],[51826,0,2],[51827,0,2],[51828,0,2],[51829,0,2],[51830,0,2],[51831,0,2],[51832,0,2],[51833,0,2],[51834,0,2],[51835,0,2],[51836,0,2],[51837,0,2],[51838,0,2],[51839,0,2],[51840,0,2],[51841,0,2],[51842,0,2],[51843,0,2],[51844,0,1],[51845,0,2],[51846,0,2],[51847,0,2],[51848,0,2],[51849,0,2],[51850,0,2],[51851,0,2],[51852,0,2],[51853,0,2],[51854,0,2],[51855,0,2],[51856,0,2],[51857,0,2],[51858,0,2],[51859,0,2],[51860,0,2],[51861,0,2],[51862,0,2],[51863,0,2],[51864,0,2],[51865,0,2],[51866,0,2],[51867,0,2],[51868,0,2],[51869,0,2],[51870,0,2],[51871,0,2],[51872,0,1],[51873,0,2],[51874,0,2],[51875,0,2],[51876,0,2],[51877,0,2],[51878,0,2],[51879,0,2],[51880,0,2],[51881,0,2],[51882,0,2],[51883,0,2],[51884,0,2],[51885,0,2],[51886,0,2],[51887,0,2],[51888,0,2],[51889,0,2],[51890,0,2],[51891,0,2],[51892,0,2],[51893,0,2],[51894,0,2],[51895,0,2],[51896,0,2],[51897,0,2],[51898,0,2],[51899,0,2],[51900,0,1],[51901,0,2],[51902,0,2],[51903,0,2],[51904,0,2],[51905,0,2],[51906,0,2],[51907,0,2],[51908,0,2],[51909,0,2],[51910,0,2],[51911,0,2],[51912,0,2],[51913,0,2],[51914,0,2],[51915,0,2],[51916,0,2],[51917,0,2],[51918,0,2],[51919,0,2],[51920,0,2],[51921,0,2],[51922,0,2],[51923,0,2],[51924,0,2],[51925,0,2],[51926,0,2],[51927,0,2],[51928,0,1],[51929,0,2],[51930,0,2],[51931,0,2],[51932,0,2],[51933,0,2],[51934,0,2],[51935,0,2],[51936,0,2],[51937,0,2],[51938,0,2],[51939,0,2],[51940,0,2],[51941,0,2],[51942,0,2],[51943,0,2],[51944,0,2],[51945,0,2],[51946,0,2],[51947,0,2],[51948,0,2],[51949,0,2],[51950,0,2],[51951,0,2],[51952,0,2],[51953,0,2],[51954,0,2],[51955,0,2],[51956,0,1],[51957,0,2],[51958,0,2],[51959,0,2],[51960,0,2],[51961,0,2],[51962,0,2],[51963,0,2],[51964,0,2],[51965,0,2],[51966,0,2],[51967,0,2],[51968,0,2],[51969,0,2],[51970,0,2],[51971,0,2],[51972,0,2],[51973,0,2],[51974,0,2],[51975,0,2],[51976,0,2],[51977,0,2],[51978,0,2],[51979,0,2],[51980,0,2],[51981,0,2],[51982,0,2],[51983,0,2],[51984,0,1],[51985,0,2],[51986,0,2],[51987,0,2],[51988,0,2],[51989,0,2],[51990,0,2],[51991,0,2],[51992,0,2],[51993,0,2],[51994,0,2],[51995,0,2],[51996,0,2],[51997,0,2],[51998,0,2],[51999,0,2],[52000,0,2],[52001,0,2],[52002,0,2],[52003,0,2],[52004,0,2],[52005,0,2],[52006,0,2],[52007,0,2],[52008,0,2],[52009,0,2],[52010,0,2],[52011,0,2],[52012,0,1],[52013,0,2],[52014,0,2],[52015,0,2],[52016,0,2],[52017,0,2],[52018,0,2],[52019,0,2],[52020,0,2],[52021,0,2],[52022,0,2],[52023,0,2],[52024,0,2],[52025,0,2],[52026,0,2],[52027,0,2],[52028,0,2],[52029,0,2],[52030,0,2],[52031,0,2],[52032,0,2],[52033,0,2],[52034,0,2],[52035,0,2],[52036,0,2],[52037,0,2],[52038,0,2],[52039,0,2],[52040,0,1],[52041,0,2],[52042,0,2],[52043,0,2],[52044,0,2],[52045,0,2],[52046,0,2],[52047,0,2],[52048,0,2],[52049,0,2],[52050,0,2],[52051,0,2],[52052,0,2],[52053,0,2],[52054,0,2],[52055,0,2],[52056,0,2],[52057,0,2],[52058,0,2],[52059,0,2],[52060,0,2],[52061,0,2],[52062,0,2],[52063,0,2],[52064,0,2],[52065,0,2],[52066,0,2],[52067,0,2],[52068,0,1],[52069,0,2],[52070,0,2],[52071,0,2],[52072,0,2],[52073,0,2],[52074,0,2],[52075,0,2],[52076,0,2],[52077,0,2],[52078,0,2],[52079,0,2],[52080,0,2],[52081,0,2],[52082,0,2],[52083,0,2],[52084,0,2],[52085,0,2],[52086,0,2],[52087,0,2],[52088,0,2],[52089,0,2],[52090,0,2],[52091,0,2],[52092,0,2],[52093,0,2],[52094,0,2],[52095,0,2],[52096,0,1],[52097,0,2],[52098,0,2],[52099,0,2],[52100,0,2],[52101,0,2],[52102,0,2],[52103,0,2],[52104,0,2],[52105,0,2],[52106,0,2],[52107,0,2],[52108,0,2],[52109,0,2],[52110,0,2],[52111,0,2],[52112,0,2],[52113,0,2],[52114,0,2],[52115,0,2],[52116,0,2],[52117,0,2],[52118,0,2],[52119,0,2],[52120,0,2],[52121,0,2],[52122,0,2],[52123,0,2],[52124,0,1],[52125,0,2],[52126,0,2],[52127,0,2],[52128,0,2],[52129,0,2],[52130,0,2],[52131,0,2],[52132,0,2],[52133,0,2],[52134,0,2],[52135,0,2],[52136,0,2],[52137,0,2],[52138,0,2],[52139,0,2],[52140,0,2],[52141,0,2],[52142,0,2],[52143,0,2],[52144,0,2],[52145,0,2],[52146,0,2],[52147,0,2],[52148,0,2],[52149,0,2],[52150,0,2],[52151,0,2],[52152,0,1],[52153,0,2],[52154,0,2],[52155,0,2],[52156,0,2],[52157,0,2],[52158,0,2],[52159,0,2],[52160,0,2],[52161,0,2],[52162,0,2],[52163,0,2],[52164,0,2],[52165,0,2],[52166,0,2],[52167,0,2],[52168,0,2],[52169,0,2],[52170,0,2],[52171,0,2],[52172,0,2],[52173,0,2],[52174,0,2],[52175,0,2],[52176,0,2],[52177,0,2],[52178,0,2],[52179,0,2],[52180,0,1],[52181,0,2],[52182,0,2],[52183,0,2],[52184,0,2],[52185,0,2],[52186,0,2],[52187,0,2],[52188,0,2],[52189,0,2],[52190,0,2],[52191,0,2],[52192,0,2],[52193,0,2],[52194,0,2],[52195,0,2],[52196,0,2],[52197,0,2],[52198,0,2],[52199,0,2],[52200,0,2],[52201,0,2],[52202,0,2],[52203,0,2],[52204,0,2],[52205,0,2],[52206,0,2],[52207,0,2],[52208,0,1],[52209,0,2],[52210,0,2],[52211,0,2],[52212,0,2],[52213,0,2],[52214,0,2],[52215,0,2],[52216,0,2],[52217,0,2],[52218,0,2],[52219,0,2],[52220,0,2],[52221,0,2],[52222,0,2],[52223,0,2],[52224,0,2],[52225,0,2],[52226,0,2],[52227,0,2],[52228,0,2],[52229,0,2],[52230,0,2],[52231,0,2],[52232,0,2],[52233,0,2],[52234,0,2],[52235,0,2],[52236,0,1],[52237,0,2],[52238,0,2],[52239,0,2],[52240,0,2],[52241,0,2],[52242,0,2],[52243,0,2],[52244,0,2],[52245,0,2],[52246,0,2],[52247,0,2],[52248,0,2],[52249,0,2],[52250,0,2],[52251,0,2],[52252,0,2],[52253,0,2],[52254,0,2],[52255,0,2],[52256,0,2],[52257,0,2],[52258,0,2],[52259,0,2],[52260,0,2],[52261,0,2],[52262,0,2],[52263,0,2],[52264,0,1],[52265,0,2],[52266,0,2],[52267,0,2],[52268,0,2],[52269,0,2],[52270,0,2],[52271,0,2],[52272,0,2],[52273,0,2],[52274,0,2],[52275,0,2],[52276,0,2],[52277,0,2],[52278,0,2],[52279,0,2],[52280,0,2],[52281,0,2],[52282,0,2],[52283,0,2],[52284,0,2],[52285,0,2],[52286,0,2],[52287,0,2],[52288,0,2],[52289,0,2],[52290,0,2],[52291,0,2],[52292,0,1],[52293,0,2],[52294,0,2],[52295,0,2],[52296,0,2],[52297,0,2],[52298,0,2],[52299,0,2],[52300,0,2],[52301,0,2],[52302,0,2],[52303,0,2],[52304,0,2],[52305,0,2],[52306,0,2],[52307,0,2],[52308,0,2],[52309,0,2],[52310,0,2],[52311,0,2],[52312,0,2],[52313,0,2],[52314,0,2],[52315,0,2],[52316,0,2],[52317,0,2],[52318,0,2],[52319,0,2],[52320,0,1],[52321,0,2],[52322,0,2],[52323,0,2],[52324,0,2],[52325,0,2],[52326,0,2],[52327,0,2],[52328,0,2],[52329,0,2],[52330,0,2],[52331,0,2],[52332,0,2],[52333,0,2],[52334,0,2],[52335,0,2],[52336,0,2],[52337,0,2],[52338,0,2],[52339,0,2],[52340,0,2],[52341,0,2],[52342,0,2],[52343,0,2],[52344,0,2],[52345,0,2],[52346,0,2],[52347,0,2],[52348,0,1],[52349,0,2],[52350,0,2],[52351,0,2],[52352,0,2],[52353,0,2],[52354,0,2],[52355,0,2],[52356,0,2],[52357,0,2],[52358,0,2],[52359,0,2],[52360,0,2],[52361,0,2],[52362,0,2],[52363,0,2],[52364,0,2],[52365,0,2],[52366,0,2],[52367,0,2],[52368,0,2],[52369,0,2],[52370,0,2],[52371,0,2],[52372,0,2],[52373,0,2],[52374,0,2],[52375,0,2],[52376,0,1],[52377,0,2],[52378,0,2],[52379,0,2],[52380,0,2],[52381,0,2],[52382,0,2],[52383,0,2],[52384,0,2],[52385,0,2],[52386,0,2],[52387,0,2],[52388,0,2],[52389,0,2],[52390,0,2],[52391,0,2],[52392,0,2],[52393,0,2],[52394,0,2],[52395,0,2],[52396,0,2],[52397,0,2],[52398,0,2],[52399,0,2],[52400,0,2],[52401,0,2],[52402,0,2],[52403,0,2],[52404,0,1],[52405,0,2],[52406,0,2],[52407,0,2],[52408,0,2],[52409,0,2],[52410,0,2],[52411,0,2],[52412,0,2],[52413,0,2],[52414,0,2],[52415,0,2],[52416,0,2],[52417,0,2],[52418,0,2],[52419,0,2],[52420,0,2],[52421,0,2],[52422,0,2],[52423,0,2],[52424,0,2],[52425,0,2],[52426,0,2],[52427,0,2],[52428,0,2],[52429,0,2],[52430,0,2],[52431,0,2],[52432,0,1],[52433,0,2],[52434,0,2],[52435,0,2],[52436,0,2],[52437,0,2],[52438,0,2],[52439,0,2],[52440,0,2],[52441,0,2],[52442,0,2],[52443,0,2],[52444,0,2],[52445,0,2],[52446,0,2],[52447,0,2],[52448,0,2],[52449,0,2],[52450,0,2],[52451,0,2],[52452,0,2],[52453,0,2],[52454,0,2],[52455,0,2],[52456,0,2],[52457,0,2],[52458,0,2],[52459,0,2],[52460,0,1],[52461,0,2],[52462,0,2],[52463,0,2],[52464,0,2],[52465,0,2],[52466,0,2],[52467,0,2],[52468,0,2],[52469,0,2],[52470,0,2],[52471,0,2],[52472,0,2],[52473,0,2],[52474,0,2],[52475,0,2],[52476,0,2],[52477,0,2],[52478,0,2],[52479,0,2],[52480,0,2],[52481,0,2],[52482,0,2],[52483,0,2],[52484,0,2],[52485,0,2],[52486,0,2],[52487,0,2],[52488,0,1],[52489,0,2],[52490,0,2],[52491,0,2],[52492,0,2],[52493,0,2],[52494,0,2],[52495,0,2],[52496,0,2],[52497,0,2],[52498,0,2],[52499,0,2],[52500,0,2],[52501,0,2],[52502,0,2],[52503,0,2],[52504,0,2],[52505,0,2],[52506,0,2],[52507,0,2],[52508,0,2],[52509,0,2],[52510,0,2],[52511,0,2],[52512,0,2],[52513,0,2],[52514,0,2],[52515,0,2],[52516,0,1],[52517,0,2],[52518,0,2],[52519,0,2],[52520,0,2],[52521,0,2],[52522,0,2],[52523,0,2],[52524,0,2],[52525,0,2],[52526,0,2],[52527,0,2],[52528,0,2],[52529,0,2],[52530,0,2],[52531,0,2],[52532,0,2],[52533,0,2],[52534,0,2],[52535,0,2],[52536,0,2],[52537,0,2],[52538,0,2],[52539,0,2],[52540,0,2],[52541,0,2],[52542,0,2],[52543,0,2],[52544,0,1],[52545,0,2],[52546,0,2],[52547,0,2],[52548,0,2],[52549,0,2],[52550,0,2],[52551,0,2],[52552,0,2],[52553,0,2],[52554,0,2],[52555,0,2],[52556,0,2],[52557,0,2],[52558,0,2],[52559,0,2],[52560,0,2],[52561,0,2],[52562,0,2],[52563,0,2],[52564,0,2],[52565,0,2],[52566,0,2],[52567,0,2],[52568,0,2],[52569,0,2],[52570,0,2],[52571,0,2],[52572,0,1],[52573,0,2],[52574,0,2],[52575,0,2],[52576,0,2],[52577,0,2],[52578,0,2],[52579,0,2],[52580,0,2],[52581,0,2],[52582,0,2],[52583,0,2],[52584,0,2],[52585,0,2],[52586,0,2],[52587,0,2],[52588,0,2],[52589,0,2],[52590,0,2],[52591,0,2],[52592,0,2],[52593,0,2],[52594,0,2],[52595,0,2],[52596,0,2],[52597,0,2],[52598,0,2],[52599,0,2],[52600,0,1],[52601,0,2],[52602,0,2],[52603,0,2],[52604,0,2],[52605,0,2],[52606,0,2],[52607,0,2],[52608,0,2],[52609,0,2],[52610,0,2],[52611,0,2],[52612,0,2],[52613,0,2],[52614,0,2],[52615,0,2],[52616,0,2],[52617,0,2],[52618,0,2],[52619,0,2],[52620,0,2],[52621,0,2],[52622,0,2],[52623,0,2],[52624,0,2],[52625,0,2],[52626,0,2],[52627,0,2],[52628,0,1],[52629,0,2],[52630,0,2],[52631,0,2],[52632,0,2],[52633,0,2],[52634,0,2],[52635,0,2],[52636,0,2],[52637,0,2],[52638,0,2],[52639,0,2],[52640,0,2],[52641,0,2],[52642,0,2],[52643,0,2],[52644,0,2],[52645,0,2],[52646,0,2],[52647,0,2],[52648,0,2],[52649,0,2],[52650,0,2],[52651,0,2],[52652,0,2],[52653,0,2],[52654,0,2],[52655,0,2],[52656,0,1],[52657,0,2],[52658,0,2],[52659,0,2],[52660,0,2],[52661,0,2],[52662,0,2],[52663,0,2],[52664,0,2],[52665,0,2],[52666,0,2],[52667,0,2],[52668,0,2],[52669,0,2],[52670,0,2],[52671,0,2],[52672,0,2],[52673,0,2],[52674,0,2],[52675,0,2],[52676,0,2],[52677,0,2],[52678,0,2],[52679,0,2],[52680,0,2],[52681,0,2],[52682,0,2],[52683,0,2],[52684,0,1],[52685,0,2],[52686,0,2],[52687,0,2],[52688,0,2],[52689,0,2],[52690,0,2],[52691,0,2],[52692,0,2],[52693,0,2],[52694,0,2],[52695,0,2],[52696,0,2],[52697,0,2],[52698,0,2],[52699,0,2],[52700,0,2],[52701,0,2],[52702,0,2],[52703,0,2],[52704,0,2],[52705,0,2],[52706,0,2],[52707,0,2],[52708,0,2],[52709,0,2],[52710,0,2],[52711,0,2],[52712,0,1],[52713,0,2],[52714,0,2],[52715,0,2],[52716,0,2],[52717,0,2],[52718,0,2],[52719,0,2],[52720,0,2],[52721,0,2],[52722,0,2],[52723,0,2],[52724,0,2],[52725,0,2],[52726,0,2],[52727,0,2],[52728,0,2],[52729,0,2],[52730,0,2],[52731,0,2],[52732,0,2],[52733,0,2],[52734,0,2],[52735,0,2],[52736,0,2],[52737,0,2],[52738,0,2],[52739,0,2],[52740,0,1],[52741,0,2],[52742,0,2],[52743,0,2],[52744,0,2],[52745,0,2],[52746,0,2],[52747,0,2],[52748,0,2],[52749,0,2],[52750,0,2],[52751,0,2],[52752,0,2],[52753,0,2],[52754,0,2],[52755,0,2],[52756,0,2],[52757,0,2],[52758,0,2],[52759,0,2],[52760,0,2],[52761,0,2],[52762,0,2],[52763,0,2],[52764,0,2],[52765,0,2],[52766,0,2],[52767,0,2],[52768,0,1],[52769,0,2],[52770,0,2],[52771,0,2],[52772,0,2],[52773,0,2],[52774,0,2],[52775,0,2],[52776,0,2],[52777,0,2],[52778,0,2],[52779,0,2],[52780,0,2],[52781,0,2],[52782,0,2],[52783,0,2],[52784,0,2],[52785,0,2],[52786,0,2],[52787,0,2],[52788,0,2],[52789,0,2],[52790,0,2],[52791,0,2],[52792,0,2],[52793,0,2],[52794,0,2],[52795,0,2],[52796,0,1],[52797,0,2],[52798,0,2],[52799,0,2],[52800,0,2],[52801,0,2],[52802,0,2],[52803,0,2],[52804,0,2],[52805,0,2],[52806,0,2],[52807,0,2],[52808,0,2],[52809,0,2],[52810,0,2],[52811,0,2],[52812,0,2],[52813,0,2],[52814,0,2],[52815,0,2],[52816,0,2],[52817,0,2],[52818,0,2],[52819,0,2],[52820,0,2],[52821,0,2],[52822,0,2],[52823,0,2],[52824,0,1],[52825,0,2],[52826,0,2],[52827,0,2],[52828,0,2],[52829,0,2],[52830,0,2],[52831,0,2],[52832,0,2],[52833,0,2],[52834,0,2],[52835,0,2],[52836,0,2],[52837,0,2],[52838,0,2],[52839,0,2],[52840,0,2],[52841,0,2],[52842,0,2],[52843,0,2],[52844,0,2],[52845,0,2],[52846,0,2],[52847,0,2],[52848,0,2],[52849,0,2],[52850,0,2],[52851,0,2],[52852,0,1],[52853,0,2],[52854,0,2],[52855,0,2],[52856,0,2],[52857,0,2],[52858,0,2],[52859,0,2],[52860,0,2],[52861,0,2],[52862,0,2],[52863,0,2],[52864,0,2],[52865,0,2],[52866,0,2],[52867,0,2],[52868,0,2],[52869,0,2],[52870,0,2],[52871,0,2],[52872,0,2],[52873,0,2],[52874,0,2],[52875,0,2],[52876,0,2],[52877,0,2],[52878,0,2],[52879,0,2],[52880,0,1],[52881,0,2],[52882,0,2],[52883,0,2],[52884,0,2],[52885,0,2],[52886,0,2],[52887,0,2],[52888,0,2],[52889,0,2],[52890,0,2],[52891,0,2],[52892,0,2],[52893,0,2],[52894,0,2],[52895,0,2],[52896,0,2],[52897,0,2],[52898,0,2],[52899,0,2],[52900,0,2],[52901,0,2],[52902,0,2],[52903,0,2],[52904,0,2],[52905,0,2],[52906,0,2],[52907,0,2],[52908,0,1],[52909,0,2],[52910,0,2],[52911,0,2],[52912,0,2],[52913,0,2],[52914,0,2],[52915,0,2],[52916,0,2],[52917,0,2],[52918,0,2],[52919,0,2],[52920,0,2],[52921,0,2],[52922,0,2],[52923,0,2],[52924,0,2],[52925,0,2],[52926,0,2],[52927,0,2],[52928,0,2],[52929,0,2],[52930,0,2],[52931,0,2],[52932,0,2],[52933,0,2],[52934,0,2],[52935,0,2],[52936,0,1],[52937,0,2],[52938,0,2],[52939,0,2],[52940,0,2],[52941,0,2],[52942,0,2],[52943,0,2],[52944,0,2],[52945,0,2],[52946,0,2],[52947,0,2],[52948,0,2],[52949,0,2],[52950,0,2],[52951,0,2],[52952,0,2],[52953,0,2],[52954,0,2],[52955,0,2],[52956,0,2],[52957,0,2],[52958,0,2],[52959,0,2],[52960,0,2],[52961,0,2],[52962,0,2],[52963,0,2],[52964,0,1],[52965,0,2],[52966,0,2],[52967,0,2],[52968,0,2],[52969,0,2],[52970,0,2],[52971,0,2],[52972,0,2],[52973,0,2],[52974,0,2],[52975,0,2],[52976,0,2],[52977,0,2],[52978,0,2],[52979,0,2],[52980,0,2],[52981,0,2],[52982,0,2],[52983,0,2],[52984,0,2],[52985,0,2],[52986,0,2],[52987,0,2],[52988,0,2],[52989,0,2],[52990,0,2],[52991,0,2],[52992,0,1],[52993,0,2],[52994,0,2],[52995,0,2],[52996,0,2],[52997,0,2],[52998,0,2],[52999,0,2],[53000,0,2],[53001,0,2],[53002,0,2],[53003,0,2],[53004,0,2],[53005,0,2],[53006,0,2],[53007,0,2],[53008,0,2],[53009,0,2],[53010,0,2],[53011,0,2],[53012,0,2],[53013,0,2],[53014,0,2],[53015,0,2],[53016,0,2],[53017,0,2],[53018,0,2],[53019,0,2],[53020,0,1],[53021,0,2],[53022,0,2],[53023,0,2],[53024,0,2],[53025,0,2],[53026,0,2],[53027,0,2],[53028,0,2],[53029,0,2],[53030,0,2],[53031,0,2],[53032,0,2],[53033,0,2],[53034,0,2],[53035,0,2],[53036,0,2],[53037,0,2],[53038,0,2],[53039,0,2],[53040,0,2],[53041,0,2],[53042,0,2],[53043,0,2],[53044,0,2],[53045,0,2],[53046,0,2],[53047,0,2],[53048,0,1],[53049,0,2],[53050,0,2],[53051,0,2],[53052,0,2],[53053,0,2],[53054,0,2],[53055,0,2],[53056,0,2],[53057,0,2],[53058,0,2],[53059,0,2],[53060,0,2],[53061,0,2],[53062,0,2],[53063,0,2],[53064,0,2],[53065,0,2],[53066,0,2],[53067,0,2],[53068,0,2],[53069,0,2],[53070,0,2],[53071,0,2],[53072,0,2],[53073,0,2],[53074,0,2],[53075,0,2],[53076,0,1],[53077,0,2],[53078,0,2],[53079,0,2],[53080,0,2],[53081,0,2],[53082,0,2],[53083,0,2],[53084,0,2],[53085,0,2],[53086,0,2],[53087,0,2],[53088,0,2],[53089,0,2],[53090,0,2],[53091,0,2],[53092,0,2],[53093,0,2],[53094,0,2],[53095,0,2],[53096,0,2],[53097,0,2],[53098,0,2],[53099,0,2],[53100,0,2],[53101,0,2],[53102,0,2],[53103,0,2],[53104,0,1],[53105,0,2],[53106,0,2],[53107,0,2],[53108,0,2],[53109,0,2],[53110,0,2],[53111,0,2],[53112,0,2],[53113,0,2],[53114,0,2],[53115,0,2],[53116,0,2],[53117,0,2],[53118,0,2],[53119,0,2],[53120,0,2],[53121,0,2],[53122,0,2],[53123,0,2],[53124,0,2],[53125,0,2],[53126,0,2],[53127,0,2],[53128,0,2],[53129,0,2],[53130,0,2],[53131,0,2],[53132,0,1],[53133,0,2],[53134,0,2],[53135,0,2],[53136,0,2],[53137,0,2],[53138,0,2],[53139,0,2],[53140,0,2],[53141,0,2],[53142,0,2],[53143,0,2],[53144,0,2],[53145,0,2],[53146,0,2],[53147,0,2],[53148,0,2],[53149,0,2],[53150,0,2],[53151,0,2],[53152,0,2],[53153,0,2],[53154,0,2],[53155,0,2],[53156,0,2],[53157,0,2],[53158,0,2],[53159,0,2],[53160,0,1],[53161,0,2],[53162,0,2],[53163,0,2],[53164,0,2],[53165,0,2],[53166,0,2],[53167,0,2],[53168,0,2],[53169,0,2],[53170,0,2],[53171,0,2],[53172,0,2],[53173,0,2],[53174,0,2],[53175,0,2],[53176,0,2],[53177,0,2],[53178,0,2],[53179,0,2],[53180,0,2],[53181,0,2],[53182,0,2],[53183,0,2],[53184,0,2],[53185,0,2],[53186,0,2],[53187,0,2],[53188,0,1],[53189,0,2],[53190,0,2],[53191,0,2],[53192,0,2],[53193,0,2],[53194,0,2],[53195,0,2],[53196,0,2],[53197,0,2],[53198,0,2],[53199,0,2],[53200,0,2],[53201,0,2],[53202,0,2],[53203,0,2],[53204,0,2],[53205,0,2],[53206,0,2],[53207,0,2],[53208,0,2],[53209,0,2],[53210,0,2],[53211,0,2],[53212,0,2],[53213,0,2],[53214,0,2],[53215,0,2],[53216,0,1],[53217,0,2],[53218,0,2],[53219,0,2],[53220,0,2],[53221,0,2],[53222,0,2],[53223,0,2],[53224,0,2],[53225,0,2],[53226,0,2],[53227,0,2],[53228,0,2],[53229,0,2],[53230,0,2],[53231,0,2],[53232,0,2],[53233,0,2],[53234,0,2],[53235,0,2],[53236,0,2],[53237,0,2],[53238,0,2],[53239,0,2],[53240,0,2],[53241,0,2],[53242,0,2],[53243,0,2],[53244,0,1],[53245,0,2],[53246,0,2],[53247,0,2],[53248,0,2],[53249,0,2],[53250,0,2],[53251,0,2],[53252,0,2],[53253,0,2],[53254,0,2],[53255,0,2],[53256,0,2],[53257,0,2],[53258,0,2],[53259,0,2],[53260,0,2],[53261,0,2],[53262,0,2],[53263,0,2],[53264,0,2],[53265,0,2],[53266,0,2],[53267,0,2],[53268,0,2],[53269,0,2],[53270,0,2],[53271,0,2],[53272,0,1],[53273,0,2],[53274,0,2],[53275,0,2],[53276,0,2],[53277,0,2],[53278,0,2],[53279,0,2],[53280,0,2],[53281,0,2],[53282,0,2],[53283,0,2],[53284,0,2],[53285,0,2],[53286,0,2],[53287,0,2],[53288,0,2],[53289,0,2],[53290,0,2],[53291,0,2],[53292,0,2],[53293,0,2],[53294,0,2],[53295,0,2],[53296,0,2],[53297,0,2],[53298,0,2],[53299,0,2],[53300,0,1],[53301,0,2],[53302,0,2],[53303,0,2],[53304,0,2],[53305,0,2],[53306,0,2],[53307,0,2],[53308,0,2],[53309,0,2],[53310,0,2],[53311,0,2],[53312,0,2],[53313,0,2],[53314,0,2],[53315,0,2],[53316,0,2],[53317,0,2],[53318,0,2],[53319,0,2],[53320,0,2],[53321,0,2],[53322,0,2],[53323,0,2],[53324,0,2],[53325,0,2],[53326,0,2],[53327,0,2],[53328,0,1],[53329,0,2],[53330,0,2],[53331,0,2],[53332,0,2],[53333,0,2],[53334,0,2],[53335,0,2],[53336,0,2],[53337,0,2],[53338,0,2],[53339,0,2],[53340,0,2],[53341,0,2],[53342,0,2],[53343,0,2],[53344,0,2],[53345,0,2],[53346,0,2],[53347,0,2],[53348,0,2],[53349,0,2],[53350,0,2],[53351,0,2],[53352,0,2],[53353,0,2],[53354,0,2],[53355,0,2],[53356,0,1],[53357,0,2],[53358,0,2],[53359,0,2],[53360,0,2],[53361,0,2],[53362,0,2],[53363,0,2],[53364,0,2],[53365,0,2],[53366,0,2],[53367,0,2],[53368,0,2],[53369,0,2],[53370,0,2],[53371,0,2],[53372,0,2],[53373,0,2],[53374,0,2],[53375,0,2],[53376,0,2],[53377,0,2],[53378,0,2],[53379,0,2],[53380,0,2],[53381,0,2],[53382,0,2],[53383,0,2],[53384,0,1],[53385,0,2],[53386,0,2],[53387,0,2],[53388,0,2],[53389,0,2],[53390,0,2],[53391,0,2],[53392,0,2],[53393,0,2],[53394,0,2],[53395,0,2],[53396,0,2],[53397,0,2],[53398,0,2],[53399,0,2],[53400,0,2],[53401,0,2],[53402,0,2],[53403,0,2],[53404,0,2],[53405,0,2],[53406,0,2],[53407,0,2],[53408,0,2],[53409,0,2],[53410,0,2],[53411,0,2],[53412,0,1],[53413,0,2],[53414,0,2],[53415,0,2],[53416,0,2],[53417,0,2],[53418,0,2],[53419,0,2],[53420,0,2],[53421,0,2],[53422,0,2],[53423,0,2],[53424,0,2],[53425,0,2],[53426,0,2],[53427,0,2],[53428,0,2],[53429,0,2],[53430,0,2],[53431,0,2],[53432,0,2],[53433,0,2],[53434,0,2],[53435,0,2],[53436,0,2],[53437,0,2],[53438,0,2],[53439,0,2],[53440,0,1],[53441,0,2],[53442,0,2],[53443,0,2],[53444,0,2],[53445,0,2],[53446,0,2],[53447,0,2],[53448,0,2],[53449,0,2],[53450,0,2],[53451,0,2],[53452,0,2],[53453,0,2],[53454,0,2],[53455,0,2],[53456,0,2],[53457,0,2],[53458,0,2],[53459,0,2],[53460,0,2],[53461,0,2],[53462,0,2],[53463,0,2],[53464,0,2],[53465,0,2],[53466,0,2],[53467,0,2],[53468,0,1],[53469,0,2],[53470,0,2],[53471,0,2],[53472,0,2],[53473,0,2],[53474,0,2],[53475,0,2],[53476,0,2],[53477,0,2],[53478,0,2],[53479,0,2],[53480,0,2],[53481,0,2],[53482,0,2],[53483,0,2],[53484,0,2],[53485,0,2],[53486,0,2],[53487,0,2],[53488,0,2],[53489,0,2],[53490,0,2],[53491,0,2],[53492,0,2],[53493,0,2],[53494,0,2],[53495,0,2],[53496,0,1],[53497,0,2],[53498,0,2],[53499,0,2],[53500,0,2],[53501,0,2],[53502,0,2],[53503,0,2],[53504,0,2],[53505,0,2],[53506,0,2],[53507,0,2],[53508,0,2],[53509,0,2],[53510,0,2],[53511,0,2],[53512,0,2],[53513,0,2],[53514,0,2],[53515,0,2],[53516,0,2],[53517,0,2],[53518,0,2],[53519,0,2],[53520,0,2],[53521,0,2],[53522,0,2],[53523,0,2],[53524,0,1],[53525,0,2],[53526,0,2],[53527,0,2],[53528,0,2],[53529,0,2],[53530,0,2],[53531,0,2],[53532,0,2],[53533,0,2],[53534,0,2],[53535,0,2],[53536,0,2],[53537,0,2],[53538,0,2],[53539,0,2],[53540,0,2],[53541,0,2],[53542,0,2],[53543,0,2],[53544,0,2],[53545,0,2],[53546,0,2],[53547,0,2],[53548,0,2],[53549,0,2],[53550,0,2],[53551,0,2],[53552,0,1],[53553,0,2],[53554,0,2],[53555,0,2],[53556,0,2],[53557,0,2],[53558,0,2],[53559,0,2],[53560,0,2],[53561,0,2],[53562,0,2],[53563,0,2],[53564,0,2],[53565,0,2],[53566,0,2],[53567,0,2],[53568,0,2],[53569,0,2],[53570,0,2],[53571,0,2],[53572,0,2],[53573,0,2],[53574,0,2],[53575,0,2],[53576,0,2],[53577,0,2],[53578,0,2],[53579,0,2],[53580,0,1],[53581,0,2],[53582,0,2],[53583,0,2],[53584,0,2],[53585,0,2],[53586,0,2],[53587,0,2],[53588,0,2],[53589,0,2],[53590,0,2],[53591,0,2],[53592,0,2],[53593,0,2],[53594,0,2],[53595,0,2],[53596,0,2],[53597,0,2],[53598,0,2],[53599,0,2],[53600,0,2],[53601,0,2],[53602,0,2],[53603,0,2],[53604,0,2],[53605,0,2],[53606,0,2],[53607,0,2],[53608,0,1],[53609,0,2],[53610,0,2],[53611,0,2],[53612,0,2],[53613,0,2],[53614,0,2],[53615,0,2],[53616,0,2],[53617,0,2],[53618,0,2],[53619,0,2],[53620,0,2],[53621,0,2],[53622,0,2],[53623,0,2],[53624,0,2],[53625,0,2],[53626,0,2],[53627,0,2],[53628,0,2],[53629,0,2],[53630,0,2],[53631,0,2],[53632,0,2],[53633,0,2],[53634,0,2],[53635,0,2],[53636,0,1],[53637,0,2],[53638,0,2],[53639,0,2],[53640,0,2],[53641,0,2],[53642,0,2],[53643,0,2],[53644,0,2],[53645,0,2],[53646,0,2],[53647,0,2],[53648,0,2],[53649,0,2],[53650,0,2],[53651,0,2],[53652,0,2],[53653,0,2],[53654,0,2],[53655,0,2],[53656,0,2],[53657,0,2],[53658,0,2],[53659,0,2],[53660,0,2],[53661,0,2],[53662,0,2],[53663,0,2],[53664,0,1],[53665,0,2],[53666,0,2],[53667,0,2],[53668,0,2],[53669,0,2],[53670,0,2],[53671,0,2],[53672,0,2],[53673,0,2],[53674,0,2],[53675,0,2],[53676,0,2],[53677,0,2],[53678,0,2],[53679,0,2],[53680,0,2],[53681,0,2],[53682,0,2],[53683,0,2],[53684,0,2],[53685,0,2],[53686,0,2],[53687,0,2],[53688,0,2],[53689,0,2],[53690,0,2],[53691,0,2],[53692,0,1],[53693,0,2],[53694,0,2],[53695,0,2],[53696,0,2],[53697,0,2],[53698,0,2],[53699,0,2],[53700,0,2],[53701,0,2],[53702,0,2],[53703,0,2],[53704,0,2],[53705,0,2],[53706,0,2],[53707,0,2],[53708,0,2],[53709,0,2],[53710,0,2],[53711,0,2],[53712,0,2],[53713,0,2],[53714,0,2],[53715,0,2],[53716,0,2],[53717,0,2],[53718,0,2],[53719,0,2],[53720,0,1],[53721,0,2],[53722,0,2],[53723,0,2],[53724,0,2],[53725,0,2],[53726,0,2],[53727,0,2],[53728,0,2],[53729,0,2],[53730,0,2],[53731,0,2],[53732,0,2],[53733,0,2],[53734,0,2],[53735,0,2],[53736,0,2],[53737,0,2],[53738,0,2],[53739,0,2],[53740,0,2],[53741,0,2],[53742,0,2],[53743,0,2],[53744,0,2],[53745,0,2],[53746,0,2],[53747,0,2],[53748,0,1],[53749,0,2],[53750,0,2],[53751,0,2],[53752,0,2],[53753,0,2],[53754,0,2],[53755,0,2],[53756,0,2],[53757,0,2],[53758,0,2],[53759,0,2],[53760,0,2],[53761,0,2],[53762,0,2],[53763,0,2],[53764,0,2],[53765,0,2],[53766,0,2],[53767,0,2],[53768,0,2],[53769,0,2],[53770,0,2],[53771,0,2],[53772,0,2],[53773,0,2],[53774,0,2],[53775,0,2],[53776,0,1],[53777,0,2],[53778,0,2],[53779,0,2],[53780,0,2],[53781,0,2],[53782,0,2],[53783,0,2],[53784,0,2],[53785,0,2],[53786,0,2],[53787,0,2],[53788,0,2],[53789,0,2],[53790,0,2],[53791,0,2],[53792,0,2],[53793,0,2],[53794,0,2],[53795,0,2],[53796,0,2],[53797,0,2],[53798,0,2],[53799,0,2],[53800,0,2],[53801,0,2],[53802,0,2],[53803,0,2],[53804,0,1],[53805,0,2],[53806,0,2],[53807,0,2],[53808,0,2],[53809,0,2],[53810,0,2],[53811,0,2],[53812,0,2],[53813,0,2],[53814,0,2],[53815,0,2],[53816,0,2],[53817,0,2],[53818,0,2],[53819,0,2],[53820,0,2],[53821,0,2],[53822,0,2],[53823,0,2],[53824,0,2],[53825,0,2],[53826,0,2],[53827,0,2],[53828,0,2],[53829,0,2],[53830,0,2],[53831,0,2],[53832,0,1],[53833,0,2],[53834,0,2],[53835,0,2],[53836,0,2],[53837,0,2],[53838,0,2],[53839,0,2],[53840,0,2],[53841,0,2],[53842,0,2],[53843,0,2],[53844,0,2],[53845,0,2],[53846,0,2],[53847,0,2],[53848,0,2],[53849,0,2],[53850,0,2],[53851,0,2],[53852,0,2],[53853,0,2],[53854,0,2],[53855,0,2],[53856,0,2],[53857,0,2],[53858,0,2],[53859,0,2],[53860,0,1],[53861,0,2],[53862,0,2],[53863,0,2],[53864,0,2],[53865,0,2],[53866,0,2],[53867,0,2],[53868,0,2],[53869,0,2],[53870,0,2],[53871,0,2],[53872,0,2],[53873,0,2],[53874,0,2],[53875,0,2],[53876,0,2],[53877,0,2],[53878,0,2],[53879,0,2],[53880,0,2],[53881,0,2],[53882,0,2],[53883,0,2],[53884,0,2],[53885,0,2],[53886,0,2],[53887,0,2],[53888,0,1],[53889,0,2],[53890,0,2],[53891,0,2],[53892,0,2],[53893,0,2],[53894,0,2],[53895,0,2],[53896,0,2],[53897,0,2],[53898,0,2],[53899,0,2],[53900,0,2],[53901,0,2],[53902,0,2],[53903,0,2],[53904,0,2],[53905,0,2],[53906,0,2],[53907,0,2],[53908,0,2],[53909,0,2],[53910,0,2],[53911,0,2],[53912,0,2],[53913,0,2],[53914,0,2],[53915,0,2],[53916,0,1],[53917,0,2],[53918,0,2],[53919,0,2],[53920,0,2],[53921,0,2],[53922,0,2],[53923,0,2],[53924,0,2],[53925,0,2],[53926,0,2],[53927,0,2],[53928,0,2],[53929,0,2],[53930,0,2],[53931,0,2],[53932,0,2],[53933,0,2],[53934,0,2],[53935,0,2],[53936,0,2],[53937,0,2],[53938,0,2],[53939,0,2],[53940,0,2],[53941,0,2],[53942,0,2],[53943,0,2],[53944,0,1],[53945,0,2],[53946,0,2],[53947,0,2],[53948,0,2],[53949,0,2],[53950,0,2],[53951,0,2],[53952,0,2],[53953,0,2],[53954,0,2],[53955,0,2],[53956,0,2],[53957,0,2],[53958,0,2],[53959,0,2],[53960,0,2],[53961,0,2],[53962,0,2],[53963,0,2],[53964,0,2],[53965,0,2],[53966,0,2],[53967,0,2],[53968,0,2],[53969,0,2],[53970,0,2],[53971,0,2],[53972,0,1],[53973,0,2],[53974,0,2],[53975,0,2],[53976,0,2],[53977,0,2],[53978,0,2],[53979,0,2],[53980,0,2],[53981,0,2],[53982,0,2],[53983,0,2],[53984,0,2],[53985,0,2],[53986,0,2],[53987,0,2],[53988,0,2],[53989,0,2],[53990,0,2],[53991,0,2],[53992,0,2],[53993,0,2],[53994,0,2],[53995,0,2],[53996,0,2],[53997,0,2],[53998,0,2],[53999,0,2],[54000,0,1],[54001,0,2],[54002,0,2],[54003,0,2],[54004,0,2],[54005,0,2],[54006,0,2],[54007,0,2],[54008,0,2],[54009,0,2],[54010,0,2],[54011,0,2],[54012,0,2],[54013,0,2],[54014,0,2],[54015,0,2],[54016,0,2],[54017,0,2],[54018,0,2],[54019,0,2],[54020,0,2],[54021,0,2],[54022,0,2],[54023,0,2],[54024,0,2],[54025,0,2],[54026,0,2],[54027,0,2],[54028,0,1],[54029,0,2],[54030,0,2],[54031,0,2],[54032,0,2],[54033,0,2],[54034,0,2],[54035,0,2],[54036,0,2],[54037,0,2],[54038,0,2],[54039,0,2],[54040,0,2],[54041,0,2],[54042,0,2],[54043,0,2],[54044,0,2],[54045,0,2],[54046,0,2],[54047,0,2],[54048,0,2],[54049,0,2],[54050,0,2],[54051,0,2],[54052,0,2],[54053,0,2],[54054,0,2],[54055,0,2],[54056,0,1],[54057,0,2],[54058,0,2],[54059,0,2],[54060,0,2],[54061,0,2],[54062,0,2],[54063,0,2],[54064,0,2],[54065,0,2],[54066,0,2],[54067,0,2],[54068,0,2],[54069,0,2],[54070,0,2],[54071,0,2],[54072,0,2],[54073,0,2],[54074,0,2],[54075,0,2],[54076,0,2],[54077,0,2],[54078,0,2],[54079,0,2],[54080,0,2],[54081,0,2],[54082,0,2],[54083,0,2],[54084,0,1],[54085,0,2],[54086,0,2],[54087,0,2],[54088,0,2],[54089,0,2],[54090,0,2],[54091,0,2],[54092,0,2],[54093,0,2],[54094,0,2],[54095,0,2],[54096,0,2],[54097,0,2],[54098,0,2],[54099,0,2],[54100,0,2],[54101,0,2],[54102,0,2],[54103,0,2],[54104,0,2],[54105,0,2],[54106,0,2],[54107,0,2],[54108,0,2],[54109,0,2],[54110,0,2],[54111,0,2],[54112,0,1],[54113,0,2],[54114,0,2],[54115,0,2],[54116,0,2],[54117,0,2],[54118,0,2],[54119,0,2],[54120,0,2],[54121,0,2],[54122,0,2],[54123,0,2],[54124,0,2],[54125,0,2],[54126,0,2],[54127,0,2],[54128,0,2],[54129,0,2],[54130,0,2],[54131,0,2],[54132,0,2],[54133,0,2],[54134,0,2],[54135,0,2],[54136,0,2],[54137,0,2],[54138,0,2],[54139,0,2],[54140,0,1],[54141,0,2],[54142,0,2],[54143,0,2],[54144,0,2],[54145,0,2],[54146,0,2],[54147,0,2],[54148,0,2],[54149,0,2],[54150,0,2],[54151,0,2],[54152,0,2],[54153,0,2],[54154,0,2],[54155,0,2],[54156,0,2],[54157,0,2],[54158,0,2],[54159,0,2],[54160,0,2],[54161,0,2],[54162,0,2],[54163,0,2],[54164,0,2],[54165,0,2],[54166,0,2],[54167,0,2],[54168,0,1],[54169,0,2],[54170,0,2],[54171,0,2],[54172,0,2],[54173,0,2],[54174,0,2],[54175,0,2],[54176,0,2],[54177,0,2],[54178,0,2],[54179,0,2],[54180,0,2],[54181,0,2],[54182,0,2],[54183,0,2],[54184,0,2],[54185,0,2],[54186,0,2],[54187,0,2],[54188,0,2],[54189,0,2],[54190,0,2],[54191,0,2],[54192,0,2],[54193,0,2],[54194,0,2],[54195,0,2],[54196,0,1],[54197,0,2],[54198,0,2],[54199,0,2],[54200,0,2],[54201,0,2],[54202,0,2],[54203,0,2],[54204,0,2],[54205,0,2],[54206,0,2],[54207,0,2],[54208,0,2],[54209,0,2],[54210,0,2],[54211,0,2],[54212,0,2],[54213,0,2],[54214,0,2],[54215,0,2],[54216,0,2],[54217,0,2],[54218,0,2],[54219,0,2],[54220,0,2],[54221,0,2],[54222,0,2],[54223,0,2],[54224,0,1],[54225,0,2],[54226,0,2],[54227,0,2],[54228,0,2],[54229,0,2],[54230,0,2],[54231,0,2],[54232,0,2],[54233,0,2],[54234,0,2],[54235,0,2],[54236,0,2],[54237,0,2],[54238,0,2],[54239,0,2],[54240,0,2],[54241,0,2],[54242,0,2],[54243,0,2],[54244,0,2],[54245,0,2],[54246,0,2],[54247,0,2],[54248,0,2],[54249,0,2],[54250,0,2],[54251,0,2],[54252,0,1],[54253,0,2],[54254,0,2],[54255,0,2],[54256,0,2],[54257,0,2],[54258,0,2],[54259,0,2],[54260,0,2],[54261,0,2],[54262,0,2],[54263,0,2],[54264,0,2],[54265,0,2],[54266,0,2],[54267,0,2],[54268,0,2],[54269,0,2],[54270,0,2],[54271,0,2],[54272,0,2],[54273,0,2],[54274,0,2],[54275,0,2],[54276,0,2],[54277,0,2],[54278,0,2],[54279,0,2],[54280,0,1],[54281,0,2],[54282,0,2],[54283,0,2],[54284,0,2],[54285,0,2],[54286,0,2],[54287,0,2],[54288,0,2],[54289,0,2],[54290,0,2],[54291,0,2],[54292,0,2],[54293,0,2],[54294,0,2],[54295,0,2],[54296,0,2],[54297,0,2],[54298,0,2],[54299,0,2],[54300,0,2],[54301,0,2],[54302,0,2],[54303,0,2],[54304,0,2],[54305,0,2],[54306,0,2],[54307,0,2],[54308,0,1],[54309,0,2],[54310,0,2],[54311,0,2],[54312,0,2],[54313,0,2],[54314,0,2],[54315,0,2],[54316,0,2],[54317,0,2],[54318,0,2],[54319,0,2],[54320,0,2],[54321,0,2],[54322,0,2],[54323,0,2],[54324,0,2],[54325,0,2],[54326,0,2],[54327,0,2],[54328,0,2],[54329,0,2],[54330,0,2],[54331,0,2],[54332,0,2],[54333,0,2],[54334,0,2],[54335,0,2],[54336,0,1],[54337,0,2],[54338,0,2],[54339,0,2],[54340,0,2],[54341,0,2],[54342,0,2],[54343,0,2],[54344,0,2],[54345,0,2],[54346,0,2],[54347,0,2],[54348,0,2],[54349,0,2],[54350,0,2],[54351,0,2],[54352,0,2],[54353,0,2],[54354,0,2],[54355,0,2],[54356,0,2],[54357,0,2],[54358,0,2],[54359,0,2],[54360,0,2],[54361,0,2],[54362,0,2],[54363,0,2],[54364,0,1],[54365,0,2],[54366,0,2],[54367,0,2],[54368,0,2],[54369,0,2],[54370,0,2],[54371,0,2],[54372,0,2],[54373,0,2],[54374,0,2],[54375,0,2],[54376,0,2],[54377,0,2],[54378,0,2],[54379,0,2],[54380,0,2],[54381,0,2],[54382,0,2],[54383,0,2],[54384,0,2],[54385,0,2],[54386,0,2],[54387,0,2],[54388,0,2],[54389,0,2],[54390,0,2],[54391,0,2],[54392,0,1],[54393,0,2],[54394,0,2],[54395,0,2],[54396,0,2],[54397,0,2],[54398,0,2],[54399,0,2],[54400,0,2],[54401,0,2],[54402,0,2],[54403,0,2],[54404,0,2],[54405,0,2],[54406,0,2],[54407,0,2],[54408,0,2],[54409,0,2],[54410,0,2],[54411,0,2],[54412,0,2],[54413,0,2],[54414,0,2],[54415,0,2],[54416,0,2],[54417,0,2],[54418,0,2],[54419,0,2],[54420,0,1],[54421,0,2],[54422,0,2],[54423,0,2],[54424,0,2],[54425,0,2],[54426,0,2],[54427,0,2],[54428,0,2],[54429,0,2],[54430,0,2],[54431,0,2],[54432,0,2],[54433,0,2],[54434,0,2],[54435,0,2],[54436,0,2],[54437,0,2],[54438,0,2],[54439,0,2],[54440,0,2],[54441,0,2],[54442,0,2],[54443,0,2],[54444,0,2],[54445,0,2],[54446,0,2],[54447,0,2],[54448,0,1],[54449,0,2],[54450,0,2],[54451,0,2],[54452,0,2],[54453,0,2],[54454,0,2],[54455,0,2],[54456,0,2],[54457,0,2],[54458,0,2],[54459,0,2],[54460,0,2],[54461,0,2],[54462,0,2],[54463,0,2],[54464,0,2],[54465,0,2],[54466,0,2],[54467,0,2],[54468,0,2],[54469,0,2],[54470,0,2],[54471,0,2],[54472,0,2],[54473,0,2],[54474,0,2],[54475,0,2],[54476,0,1],[54477,0,2],[54478,0,2],[54479,0,2],[54480,0,2],[54481,0,2],[54482,0,2],[54483,0,2],[54484,0,2],[54485,0,2],[54486,0,2],[54487,0,2],[54488,0,2],[54489,0,2],[54490,0,2],[54491,0,2],[54492,0,2],[54493,0,2],[54494,0,2],[54495,0,2],[54496,0,2],[54497,0,2],[54498,0,2],[54499,0,2],[54500,0,2],[54501,0,2],[54502,0,2],[54503,0,2],[54504,0,1],[54505,0,2],[54506,0,2],[54507,0,2],[54508,0,2],[54509,0,2],[54510,0,2],[54511,0,2],[54512,0,2],[54513,0,2],[54514,0,2],[54515,0,2],[54516,0,2],[54517,0,2],[54518,0,2],[54519,0,2],[54520,0,2],[54521,0,2],[54522,0,2],[54523,0,2],[54524,0,2],[54525,0,2],[54526,0,2],[54527,0,2],[54528,0,2],[54529,0,2],[54530,0,2],[54531,0,2],[54532,0,1],[54533,0,2],[54534,0,2],[54535,0,2],[54536,0,2],[54537,0,2],[54538,0,2],[54539,0,2],[54540,0,2],[54541,0,2],[54542,0,2],[54543,0,2],[54544,0,2],[54545,0,2],[54546,0,2],[54547,0,2],[54548,0,2],[54549,0,2],[54550,0,2],[54551,0,2],[54552,0,2],[54553,0,2],[54554,0,2],[54555,0,2],[54556,0,2],[54557,0,2],[54558,0,2],[54559,0,2],[54560,0,1],[54561,0,2],[54562,0,2],[54563,0,2],[54564,0,2],[54565,0,2],[54566,0,2],[54567,0,2],[54568,0,2],[54569,0,2],[54570,0,2],[54571,0,2],[54572,0,2],[54573,0,2],[54574,0,2],[54575,0,2],[54576,0,2],[54577,0,2],[54578,0,2],[54579,0,2],[54580,0,2],[54581,0,2],[54582,0,2],[54583,0,2],[54584,0,2],[54585,0,2],[54586,0,2],[54587,0,2],[54588,0,1],[54589,0,2],[54590,0,2],[54591,0,2],[54592,0,2],[54593,0,2],[54594,0,2],[54595,0,2],[54596,0,2],[54597,0,2],[54598,0,2],[54599,0,2],[54600,0,2],[54601,0,2],[54602,0,2],[54603,0,2],[54604,0,2],[54605,0,2],[54606,0,2],[54607,0,2],[54608,0,2],[54609,0,2],[54610,0,2],[54611,0,2],[54612,0,2],[54613,0,2],[54614,0,2],[54615,0,2],[54616,0,1],[54617,0,2],[54618,0,2],[54619,0,2],[54620,0,2],[54621,0,2],[54622,0,2],[54623,0,2],[54624,0,2],[54625,0,2],[54626,0,2],[54627,0,2],[54628,0,2],[54629,0,2],[54630,0,2],[54631,0,2],[54632,0,2],[54633,0,2],[54634,0,2],[54635,0,2],[54636,0,2],[54637,0,2],[54638,0,2],[54639,0,2],[54640,0,2],[54641,0,2],[54642,0,2],[54643,0,2],[54644,0,1],[54645,0,2],[54646,0,2],[54647,0,2],[54648,0,2],[54649,0,2],[54650,0,2],[54651,0,2],[54652,0,2],[54653,0,2],[54654,0,2],[54655,0,2],[54656,0,2],[54657,0,2],[54658,0,2],[54659,0,2],[54660,0,2],[54661,0,2],[54662,0,2],[54663,0,2],[54664,0,2],[54665,0,2],[54666,0,2],[54667,0,2],[54668,0,2],[54669,0,2],[54670,0,2],[54671,0,2],[54672,0,1],[54673,0,2],[54674,0,2],[54675,0,2],[54676,0,2],[54677,0,2],[54678,0,2],[54679,0,2],[54680,0,2],[54681,0,2],[54682,0,2],[54683,0,2],[54684,0,2],[54685,0,2],[54686,0,2],[54687,0,2],[54688,0,2],[54689,0,2],[54690,0,2],[54691,0,2],[54692,0,2],[54693,0,2],[54694,0,2],[54695,0,2],[54696,0,2],[54697,0,2],[54698,0,2],[54699,0,2],[54700,0,1],[54701,0,2],[54702,0,2],[54703,0,2],[54704,0,2],[54705,0,2],[54706,0,2],[54707,0,2],[54708,0,2],[54709,0,2],[54710,0,2],[54711,0,2],[54712,0,2],[54713,0,2],[54714,0,2],[54715,0,2],[54716,0,2],[54717,0,2],[54718,0,2],[54719,0,2],[54720,0,2],[54721,0,2],[54722,0,2],[54723,0,2],[54724,0,2],[54725,0,2],[54726,0,2],[54727,0,2],[54728,0,1],[54729,0,2],[54730,0,2],[54731,0,2],[54732,0,2],[54733,0,2],[54734,0,2],[54735,0,2],[54736,0,2],[54737,0,2],[54738,0,2],[54739,0,2],[54740,0,2],[54741,0,2],[54742,0,2],[54743,0,2],[54744,0,2],[54745,0,2],[54746,0,2],[54747,0,2],[54748,0,2],[54749,0,2],[54750,0,2],[54751,0,2],[54752,0,2],[54753,0,2],[54754,0,2],[54755,0,2],[54756,0,1],[54757,0,2],[54758,0,2],[54759,0,2],[54760,0,2],[54761,0,2],[54762,0,2],[54763,0,2],[54764,0,2],[54765,0,2],[54766,0,2],[54767,0,2],[54768,0,2],[54769,0,2],[54770,0,2],[54771,0,2],[54772,0,2],[54773,0,2],[54774,0,2],[54775,0,2],[54776,0,2],[54777,0,2],[54778,0,2],[54779,0,2],[54780,0,2],[54781,0,2],[54782,0,2],[54783,0,2],[54784,0,1],[54785,0,2],[54786,0,2],[54787,0,2],[54788,0,2],[54789,0,2],[54790,0,2],[54791,0,2],[54792,0,2],[54793,0,2],[54794,0,2],[54795,0,2],[54796,0,2],[54797,0,2],[54798,0,2],[54799,0,2],[54800,0,2],[54801,0,2],[54802,0,2],[54803,0,2],[54804,0,2],[54805,0,2],[54806,0,2],[54807,0,2],[54808,0,2],[54809,0,2],[54810,0,2],[54811,0,2],[54812,0,1],[54813,0,2],[54814,0,2],[54815,0,2],[54816,0,2],[54817,0,2],[54818,0,2],[54819,0,2],[54820,0,2],[54821,0,2],[54822,0,2],[54823,0,2],[54824,0,2],[54825,0,2],[54826,0,2],[54827,0,2],[54828,0,2],[54829,0,2],[54830,0,2],[54831,0,2],[54832,0,2],[54833,0,2],[54834,0,2],[54835,0,2],[54836,0,2],[54837,0,2],[54838,0,2],[54839,0,2],[54840,0,1],[54841,0,2],[54842,0,2],[54843,0,2],[54844,0,2],[54845,0,2],[54846,0,2],[54847,0,2],[54848,0,2],[54849,0,2],[54850,0,2],[54851,0,2],[54852,0,2],[54853,0,2],[54854,0,2],[54855,0,2],[54856,0,2],[54857,0,2],[54858,0,2],[54859,0,2],[54860,0,2],[54861,0,2],[54862,0,2],[54863,0,2],[54864,0,2],[54865,0,2],[54866,0,2],[54867,0,2],[54868,0,1],[54869,0,2],[54870,0,2],[54871,0,2],[54872,0,2],[54873,0,2],[54874,0,2],[54875,0,2],[54876,0,2],[54877,0,2],[54878,0,2],[54879,0,2],[54880,0,2],[54881,0,2],[54882,0,2],[54883,0,2],[54884,0,2],[54885,0,2],[54886,0,2],[54887,0,2],[54888,0,2],[54889,0,2],[54890,0,2],[54891,0,2],[54892,0,2],[54893,0,2],[54894,0,2],[54895,0,2],[54896,0,1],[54897,0,2],[54898,0,2],[54899,0,2],[54900,0,2],[54901,0,2],[54902,0,2],[54903,0,2],[54904,0,2],[54905,0,2],[54906,0,2],[54907,0,2],[54908,0,2],[54909,0,2],[54910,0,2],[54911,0,2],[54912,0,2],[54913,0,2],[54914,0,2],[54915,0,2],[54916,0,2],[54917,0,2],[54918,0,2],[54919,0,2],[54920,0,2],[54921,0,2],[54922,0,2],[54923,0,2],[54924,0,1],[54925,0,2],[54926,0,2],[54927,0,2],[54928,0,2],[54929,0,2],[54930,0,2],[54931,0,2],[54932,0,2],[54933,0,2],[54934,0,2],[54935,0,2],[54936,0,2],[54937,0,2],[54938,0,2],[54939,0,2],[54940,0,2],[54941,0,2],[54942,0,2],[54943,0,2],[54944,0,2],[54945,0,2],[54946,0,2],[54947,0,2],[54948,0,2],[54949,0,2],[54950,0,2],[54951,0,2],[54952,0,1],[54953,0,2],[54954,0,2],[54955,0,2],[54956,0,2],[54957,0,2],[54958,0,2],[54959,0,2],[54960,0,2],[54961,0,2],[54962,0,2],[54963,0,2],[54964,0,2],[54965,0,2],[54966,0,2],[54967,0,2],[54968,0,2],[54969,0,2],[54970,0,2],[54971,0,2],[54972,0,2],[54973,0,2],[54974,0,2],[54975,0,2],[54976,0,2],[54977,0,2],[54978,0,2],[54979,0,2],[54980,0,1],[54981,0,2],[54982,0,2],[54983,0,2],[54984,0,2],[54985,0,2],[54986,0,2],[54987,0,2],[54988,0,2],[54989,0,2],[54990,0,2],[54991,0,2],[54992,0,2],[54993,0,2],[54994,0,2],[54995,0,2],[54996,0,2],[54997,0,2],[54998,0,2],[54999,0,2],[55000,0,2],[55001,0,2],[55002,0,2],[55003,0,2],[55004,0,2],[55005,0,2],[55006,0,2],[55007,0,2],[55008,0,1],[55009,0,2],[55010,0,2],[55011,0,2],[55012,0,2],[55013,0,2],[55014,0,2],[55015,0,2],[55016,0,2],[55017,0,2],[55018,0,2],[55019,0,2],[55020,0,2],[55021,0,2],[55022,0,2],[55023,0,2],[55024,0,2],[55025,0,2],[55026,0,2],[55027,0,2],[55028,0,2],[55029,0,2],[55030,0,2],[55031,0,2],[55032,0,2],[55033,0,2],[55034,0,2],[55035,0,2],[55036,0,1],[55037,0,2],[55038,0,2],[55039,0,2],[55040,0,2],[55041,0,2],[55042,0,2],[55043,0,2],[55044,0,2],[55045,0,2],[55046,0,2],[55047,0,2],[55048,0,2],[55049,0,2],[55050,0,2],[55051,0,2],[55052,0,2],[55053,0,2],[55054,0,2],[55055,0,2],[55056,0,2],[55057,0,2],[55058,0,2],[55059,0,2],[55060,0,2],[55061,0,2],[55062,0,2],[55063,0,2],[55064,0,1],[55065,0,2],[55066,0,2],[55067,0,2],[55068,0,2],[55069,0,2],[55070,0,2],[55071,0,2],[55072,0,2],[55073,0,2],[55074,0,2],[55075,0,2],[55076,0,2],[55077,0,2],[55078,0,2],[55079,0,2],[55080,0,2],[55081,0,2],[55082,0,2],[55083,0,2],[55084,0,2],[55085,0,2],[55086,0,2],[55087,0,2],[55088,0,2],[55089,0,2],[55090,0,2],[55091,0,2],[55092,0,1],[55093,0,2],[55094,0,2],[55095,0,2],[55096,0,2],[55097,0,2],[55098,0,2],[55099,0,2],[55100,0,2],[55101,0,2],[55102,0,2],[55103,0,2],[55104,0,2],[55105,0,2],[55106,0,2],[55107,0,2],[55108,0,2],[55109,0,2],[55110,0,2],[55111,0,2],[55112,0,2],[55113,0,2],[55114,0,2],[55115,0,2],[55116,0,2],[55117,0,2],[55118,0,2],[55119,0,2],[55120,0,1],[55121,0,2],[55122,0,2],[55123,0,2],[55124,0,2],[55125,0,2],[55126,0,2],[55127,0,2],[55128,0,2],[55129,0,2],[55130,0,2],[55131,0,2],[55132,0,2],[55133,0,2],[55134,0,2],[55135,0,2],[55136,0,2],[55137,0,2],[55138,0,2],[55139,0,2],[55140,0,2],[55141,0,2],[55142,0,2],[55143,0,2],[55144,0,2],[55145,0,2],[55146,0,2],[55147,0,2],[55148,0,1],[55149,0,2],[55150,0,2],[55151,0,2],[55152,0,2],[55153,0,2],[55154,0,2],[55155,0,2],[55156,0,2],[55157,0,2],[55158,0,2],[55159,0,2],[55160,0,2],[55161,0,2],[55162,0,2],[55163,0,2],[55164,0,2],[55165,0,2],[55166,0,2],[55167,0,2],[55168,0,2],[55169,0,2],[55170,0,2],[55171,0,2],[55172,0,2],[55173,0,2],[55174,0,2],[55175,0,2],[55176,0,1],[55177,0,2],[55178,0,2],[55179,0,2],[55180,0,2],[55181,0,2],[55182,0,2],[55183,0,2],[55184,0,2],[55185,0,2],[55186,0,2],[55187,0,2],[55188,0,2],[55189,0,2],[55190,0,2],[55191,0,2],[55192,0,2],[55193,0,2],[55194,0,2],[55195,0,2],[55196,0,2],[55197,0,2],[55198,0,2],[55199,0,2],[55200,0,2],[55201,0,2],[55202,0,2],[55203,0,2],[64285,0,1],[64286,1,1],[64287,0,1],[64298,0,1],[64299,0,1],[64300,0,2],[64301,0,2],[64302,0,1],[64303,0,1],[64304,0,1],[64305,0,1],[64306,0,1],[64307,0,1],[64308,0,1],[64309,0,1],[64310,0,1],[64312,0,1],[64313,0,1],[64314,0,1],[64315,0,1],[64316,0,1],[64318,0,1],[64320,0,1],[64321,0,1],[64323,0,1],[64324,0,1],[64326,0,1],[64327,0,1],[64328,0,1],[64329,0,1],[64330,0,1],[64331,0,1],[64332,0,1],[64333,0,1],[64334,0,1],[64420,0,1],[64421,0,1],[64432,0,1],[64433,0,1],[64603,0,1],[64604,0,1],[64605,0,1],[64606,0,2],[64607,0,2],[64608,0,2],[64609,0,2],[64610,0,2],[64611,0,2],[64656,0,1],[64729,0,1],[64754,0,2],[64755,0,2],[64756,0,2],[64828,0,1],[64829,0,1],[65056,1,1],[65057,1,1],[65058,1,1],[65059,1,1],[65060,1,1],[65061,1,1],[65062,1,1],[65063,1,1],[65064,1,1],[65065,1,1],[65066,1,1],[65067,1,1],[65068,1,1],[65069,1,1],[65070,1,1],[65071,1,1],[65097,0,1],[65098,0,1],[65099,0,1],[65100,0,1],[65136,0,1],[65137,0,1],[65138,0,1],[65140,0,1],[65142,0,1],[65143,0,1],[65144,0,1],[65145,0,1],[65146,0,1],[65147,0,1],[65148,0,1],[65149,0,1],[65150,0,1],[65151,0,1],[65153,0,1],[65154,0,1],[65155,0,1],[65156,0,1],[65157,0,1],[65158,0,1],[65159,0,1],[65160,0,1],[65161,0,1],[65162,0,1],[65163,0,1],[65164,0,1],[65269,0,1],[65270,0,1],[65271,0,1],[65272,0,1],[65273,0,1],[65274,0,1],[65438,1,1],[65439,1,1],[65443,1,1],[65445,1,1],[65446,1,1],[65450,1,1],[65451,1,1],[65452,1,1],[65453,1,1],[65454,1,1],[65455,1,1],[65474,1,1],[65475,1,1],[65476,1,1],[65477,1,1],[65478,1,1],[65479,1,1],[65482,1,1],[65483,1,1],[65484,1,1],[65485,1,1],[65486,1,1],[65487,1,1],[65490,1,1],[65491,1,1],[65492,1,1],[65493,1,1],[65494,1,1],[65495,1,1],[65498,1,1],[65499,1,1],[65500,1,1],[65507,0,1],[66045,1,1],[66272,1,1],[66422,1,1],[66423,1,1],[66424,1,1],[66425,1,1],[66426,1,1],[68109,1,1],[68111,1,1],[68152,1,1],[68153,1,1],[68154,1,1],[68159,1,1],[68325,1,1],[68326,1,1],[68900,1,1],[68901,1,1],[68902,1,1],[68903,1,1],[69291,1,1],[69292,1,1],[69373,1,1],[69374,1,1],[69375,1,1],[69446,1,1],[69447,1,1],[69448,1,1],[69449,1,1],[69450,1,1],[69451,1,1],[69452,1,1],[69453,1,1],[69454,1,1],[69455,1,1],[69456,1,1],[69506,1,1],[69507,1,1],[69508,1,1],[69509,1,1],[69702,1,1],[69744,1,1],[69759,1,1],[69786,0,1],[69788,0,1],[69803,0,1],[69817,1,1],[69818,1,1],[69888,1,1],[69889,1,1],[69890,1,1],[69927,1,1],[69934,0,1],[69935,0,1],[69939,1,1],[69940,1,1],[70003,1,1],[70080,1,1],[70090,1,1],[70197,1,1],[70198,1,1],[70377,1,1],[70378,1,1],[70459,1,1],[70460,1,1],[70462,1,1],[70475,0,1],[70476,0,1],[70477,1,1],[70487,1,1],[70502,1,1],[70503,1,1],[70504,1,1],[70505,1,1],[70506,1,1],[70507,1,1],[70508,1,1],[70512,1,1],[70513,1,1],[70514,1,1],[70515,1,1],[70516,1,1],[70722,1,1],[70726,1,1],[70750,1,1],[70832,1,1],[70842,1,1],[70843,0,1],[70844,0,1],[70845,1,1],[70846,0,1],[70850,1,1],[70851,1,1],[71087,1,1],[71098,0,1],[71099,0,1],[71103,1,1],[71104,1,1],[71231,1,1],[71350,1,1],[71351,1,1],[71467,1,1],[71737,1,1],[71738,1,1],[71984,1,1],[71992,0,1],[71997,1,1],[71998,1,1],[72003,1,1],[72160,1,1],[72244,1,1],[72263,1,1],[72345,1,1],[72767,1,1],[73026,1,1],[73028,1,1],[73029,1,1],[73111,1,1],[73537,1,1],[73538,1,1],[92912,1,1],[92913,1,1],[92914,1,1],[92915,1,1],[92916,1,1],[92976,1,1],[92977,1,1],[92978,1,1],[92979,1,1],[92980,1,1],[92981,1,1],[92982,1,1],[94192,1,1],[94193,1,1],[113822,1,1],[119134,0,1],[119135,0,1],[119136,0,2],[119137,0,2],[119138,0,2],[119139,0,2],[119140,0,2],[119141,1,1],[119142,1,1],[119143,1,1],[119144,1,1],[119145,1,1],[119149,1,1],[119150,1,1],[119151,1,1],[119152,1,1],[119153,1,1],[119154,1,1],[119163,1,1],[119164,1,1],[119165,1,1],[119166,1,1],[119167,1,1],[119168,1,1],[119169,1,1],[119170,1,1],[119173,1,1],[119174,1,1],[119175,1,1],[119176,1,1],[119177,1,1],[119178,1,1],[119179,1,1],[119210,1,1],[119211,1,1],[119212,1,1],[119213,1,1],[119227,0,1],[119228,0,1],[119229,0,2],[119230,0,2],[119231,0,2],[119232,0,2],[119362,1,1],[119363,1,1],[119364,1,1],[122880,1,1],[122881,1,1],[122882,1,1],[122883,1,1],[122884,1,1],[122885,1,1],[122886,1,1],[122888,1,1],[122889,1,1],[122890,1,1],[122891,1,1],[122892,1,1],[122893,1,1],[122894,1,1],[122895,1,1],[122896,1,1],[122897,1,1],[122898,1,1],[122899,1,1],[122900,1,1],[122901,1,1],[122902,1,1],[122903,1,1],[122904,1,1],[122907,1,1],[122908,1,1],[122909,1,1],[122910,1,1],[122911,1,1],[122912,1,1],[122913,1,1],[122915,1,1],[122916,1,1],[122918,1,1],[122919,1,1],[122920,1,1],[122921,1,1],[122922,1,1],[123023,1,1],[123184,1,1],[123185,1,1],[123186,1,1],[123187,1,1],[123188,1,1],[123189,1,1],[123190,1,1],[123566,1,1],[123628,1,1],[123629,1,1],[123630,1,1],[123631,1,1],[124140,1,1],[124141,1,1],[124142,1,1],[124143,1,1],[125136,1,1],[125137,1,1],[125138,1,1],[125139,1,1],[125140,1,1],[125141,1,1],[125142,1,1],[125252,1,1],[125253,1,1],[125254,1,1],[125255,1,1],[125256,1,1],[125257,1,1],[125258,1,1],[127507,0,1]]} diff --git a/packages/skill-tree/src/byte-source.ts b/packages/skill-tree/src/byte-source.ts index 008192b..893444a 100644 --- a/packages/skill-tree/src/byte-source.ts +++ b/packages/skill-tree/src/byte-source.ts @@ -98,11 +98,20 @@ export class StreamReader { private chunk: Uint8Array = new Uint8Array(0); private offset = 0; private ended = false; + private consumed = 0; - constructor(source: AsyncIterable) { + /** `budget` caps the total bytes taken from the stream: headers, skipped data and file content alike. */ + constructor(source: AsyncIterable, private readonly budget = Infinity) { this.it = source[Symbol.asyncIterator](); } + private spend(n: number): void { + this.consumed += n; + if (this.consumed > this.budget) { + throw new SkillTreeError(ERROR_CODES.limitExceeded, "", `archive data exceeds ${this.budget} bytes`); + } + } + private async nextChunk(): Promise { while (!this.ended) { const next = await this.it.next(); @@ -132,6 +141,7 @@ export class StreamReader { throw readFailed(`stream ends after ${filled} of ${n} bytes`); } const take = Math.min(n - filled, this.chunk.length - this.offset); + this.spend(take); out.set(this.chunk.subarray(this.offset, this.offset + take), filled); this.offset += take; filled += take; @@ -147,6 +157,7 @@ export class StreamReader { throw readFailed(`stream ends ${left} bytes before the end of an entry`); } const take = Math.min(left, this.chunk.length - this.offset); + this.spend(take); const piece = this.chunk.subarray(this.offset, this.offset + take); this.offset += take; left -= take; diff --git a/packages/skill-tree/src/collision.ts b/packages/skill-tree/src/collision.ts index 395edd4..37a097e 100644 --- a/packages/skill-tree/src/collision.ts +++ b/packages/skill-tree/src/collision.ts @@ -45,9 +45,10 @@ export function checkFileDirs(files: readonly string[], dirEntries: readonly str * so a given entry list always reports the same path. */ export function checkCollisions(files: readonly string[], dirEntries: readonly string[]): SkillTreeError | null { + const keys = files.map(foldKey); const byKey = new Map(); - for (const file of files) { - const key = foldKey(file); + for (const [i, file] of files.entries()) { + const key = keys[i]!; const prior = byKey.get(key); if (prior !== undefined) { return treeError(ERROR_CODES.caseCollision, file, `collides with ${JSON.stringify(prior)} under case folding`); @@ -56,9 +57,12 @@ export function checkCollisions(files: readonly string[], dirEntries: readonly s } const foldedDirs = new Map(); let dirConflict: SkillTreeError | null = null; - for (const file of files) { - for (const dir of dirPrefixes(file)) { - const key = foldKey(dir); + for (const [i, file] of files.entries()) { + // "/" is a starter that neither folds, decomposes nor reorders, and nothing + // folds or decomposes to it, so the key's prefixes are the prefixes' keys. + const dirKeys = dirPrefixes(keys[i]!); + for (const [j, dir] of dirPrefixes(file).entries()) { + const key = dirKeys[j]!; const prior = foldedDirs.get(key); if (prior === undefined) foldedDirs.set(key, { dir, file }); else if (prior.dir !== dir && dirConflict === null) { @@ -70,8 +74,8 @@ export function checkCollisions(files: readonly string[], dirEntries: readonly s } } } - for (const file of files) { - const owner = foldedDirs.get(foldKey(file)); + for (const [i, file] of files.entries()) { + const owner = foldedDirs.get(keys[i]!); if (owner !== undefined) { return treeError(ERROR_CODES.caseCollision, file, `collides under case folding with the directory of ${JSON.stringify(owner.file)}`); } diff --git a/packages/skill-tree/src/entry-collector.ts b/packages/skill-tree/src/entry-collector.ts index 0951970..90ac8ba 100644 --- a/packages/skill-tree/src/entry-collector.ts +++ b/packages/skill-tree/src/entry-collector.ts @@ -24,27 +24,44 @@ export class EntryCollector { } } - /** Hashes one regular file's content as it streams, counting it toward the file and byte limits. */ - async hashFile(path: string, content: AsyncIterable): Promise<{ size: number; sha256: string }> { + private countBytes(path: string, size: number, added: number): void { + this.total += added; + if (!(size <= this.limits.maxFileSize)) { + throw treeError(ERROR_CODES.limitExceeded, path, `file exceeds ${this.limits.maxFileSize} bytes`); + } + if (this.total > this.limits.maxTreeSize) { + throw treeError(ERROR_CODES.limitExceeded, path, `tree exceeds ${this.limits.maxTreeSize} bytes`); + } + } + + private countFileEntry(path: string): void { if (++this.files > this.limits.maxFiles) { throw treeError(ERROR_CODES.limitExceeded, path, `tree has more than ${this.limits.maxFiles} files`); } + } + + /** Hashes one regular file's content as it streams, counting it toward the file and byte limits. */ + async hashFile(path: string, content: AsyncIterable): Promise<{ size: number; sha256: string }> { + this.countFileEntry(path); const hash = createSha256(); let size = 0; for await (const chunk of content) { size += chunk.length; - this.total += chunk.length; - if (size > this.limits.maxFileSize) { - throw treeError(ERROR_CODES.limitExceeded, path, `file exceeds ${this.limits.maxFileSize} bytes`); - } - if (this.total > this.limits.maxTreeSize) { - throw treeError(ERROR_CODES.limitExceeded, path, `tree exceeds ${this.limits.maxTreeSize} bytes`); - } + this.countBytes(path, size, chunk.length); hash.update(chunk); } return { size, sha256: hash.hex() }; } + /** + * Counts a regular file whose content is not hashed (a sparse file, which + * is rejected later) at its logical size, as the limit step sees it. + */ + countFile(path: string, size: number): void { + this.countFileEntry(path); + this.countBytes(path, size, Math.max(size, 0)); + } + add(entry: TreeEntry): void { this.entries.push(entry); } diff --git a/packages/skill-tree/src/tar-header.ts b/packages/skill-tree/src/tar-header.ts index 86ce14b..1902771 100644 --- a/packages/skill-tree/src/tar-header.ts +++ b/packages/skill-tree/src/tar-header.ts @@ -9,53 +9,77 @@ export const BLOCK = 512; /** Go archive/tar caps PAX and GNU long-name payloads at 1 MiB. */ export const MAX_SPECIAL_FILE_SIZE = 1 << 20; +const INT64_MIN = -(1n << 63n); +const INT64_MAX = (1n << 63n) - 1n; + +export type TarFormat = "v7" | "ustar" | "star" | "gnu"; + export interface RawHeader { - /** Raw name bytes (ustar prefix joined), before any PAX or GNU override. */ + /** Raw name bytes (prefix joined), before any PAX or GNU override. */ name: Uint8Array; linkname: Uint8Array; + /** Header mode; may be negative or beyond 2^53 (then imprecise), which `headerMode` rejects. */ mode: number; + /** Header size; negative when a base-256 field says so. */ size: number; typeflag: string; - /** Old GNU sparse ('S') extension flag at offset 482. */ - gnuExtended: boolean; + format: TarFormat; + /** The raw header block, kept for the old GNU sparse map. */ + block: Uint8Array; } -const latin = new TextDecoder("latin1"); +/** Bytes as a string of char codes 0-255; only ever matched against ASCII patterns. */ +export function byteString(bytes: Uint8Array): string { + let out = ""; + for (const b of bytes) out += String.fromCharCode(b); + return out; +} -function cString(block: Uint8Array, start: number, length: number): Uint8Array { - const field = block.subarray(start, start + length); +/** Go parser.parseString: the bytes before the first NUL. */ +function cString(field: Uint8Array): Uint8Array { const nul = field.indexOf(0); return nul < 0 ? field : field.subarray(0, nul); } -function trimField(field: Uint8Array): string { - return latin.decode(field).replace(/^[ \0]+|[ \0]+$/g, ""); +const isPad = (b: number) => b === 0x20 || b === 0; + +/** Go parseOctal: trim spaces and NULs, cut at a NUL, then an unsigned 64-bit octal number. */ +function parseOctal(field: Uint8Array, what: string): bigint { + let start = 0; + let end = field.length; + while (start < end && isPad(field[start]!)) start++; + while (end > start && isPad(field[end - 1]!)) end--; + if (start === end) return 0n; + const text = byteString(cString(field.subarray(start, end))); + if (!/^[0-7]+$/.test(text)) throw readFailed(`invalid octal ${what} ${JSON.stringify(text)}`); + const value = BigInt(`0o${text}`); + if (value >> 64n > 0n) throw readFailed(`${what} overflows`); + return BigInt.asIntN(64, value); } -/** Octal or GNU base-256 numeric field, like Go's parseNumeric. */ -export function parseNumeric(field: Uint8Array, what: string): number { +/** Go parseNumeric: octal, or GNU base-256 two's complement (negative values included) within int64. */ +export function parseNumeric(field: Uint8Array, what: string): bigint { const first = field[0] ?? 0; - if (first & 0x80) { - if (first & 0x40) throw readFailed(`negative base-256 ${what}`); - let value = first & 0x3f; - for (const byte of field.subarray(1)) { - value = value * 256 + byte; - if (!Number.isSafeInteger(value)) throw readFailed(`${what} overflows`); - } - return value; + if ((first & 0x80) === 0) return parseOctal(field, what); + const inv = first & 0x40 ? 0xff : 0; + let x = 0n; + for (let i = 0; i < field.length; i++) { + let c = field[i]! ^ inv; + if (i === 0) c &= 0x7f; + if (x >> 56n > 0n) throw readFailed(`${what} overflows`); + x = (x << 8n) | BigInt(c); } - const text = trimField(field); - if (text === "") return 0; - if (!/^[0-7]+$/.test(text)) throw readFailed(`invalid octal ${what} ${JSON.stringify(text)}`); - const value = parseInt(text, 8); - if (!Number.isSafeInteger(value)) throw readFailed(`${what} overflows`); - return value; + if (x >> 63n > 0n) throw readFailed(`${what} overflows`); + return inv ? -x - 1n : x; } function checksumMatches(block: Uint8Array): boolean { - const stored = trimField(block.subarray(148, 156)); - if (!/^[0-7]+$/.test(stored)) return false; - const want = parseInt(stored, 8); + let want: bigint; + try { + want = parseOctal(block.subarray(148, 156), "checksum"); + } catch { + return false; + } let unsigned = 0; let signed = 0; for (let i = 0; i < BLOCK; i++) { @@ -63,7 +87,7 @@ function checksumMatches(block: Uint8Array): boolean { unsigned += byte; signed += byte > 127 ? byte - 256 : byte; } - return want === unsigned || want === signed; + return want === BigInt(unsigned) || want === BigInt(signed); } function startsWith(block: Uint8Array, offset: number, text: string): boolean { @@ -71,82 +95,166 @@ function startsWith(block: Uint8Array, offset: number, text: string): boolean { return true; } +/** Go block.getFormat after the checksum: the 6-byte magic decides, the USTAR version is not checked. */ +function formatOf(block: Uint8Array): TarFormat { + if (startsWith(block, 257, "ustar\u0000")) return startsWith(block, 508, "tar\u0000") ? "star" : "ustar"; + if (startsWith(block, 257, "ustar ") && startsWith(block, 263, " \u0000")) return "gnu"; + return "v7"; +} + export function isZeroBlock(block: Uint8Array): boolean { return block.every((b) => b === 0); } +function joinPrefix(prefix: Uint8Array, name: Uint8Array): Uint8Array { + if (prefix.length === 0) return name; + const joined = new Uint8Array(prefix.length + 1 + name.length); + joined.set(prefix); + joined[prefix.length] = 0x2f; + joined.set(name, prefix.length + 1); + return joined; +} + +/** + * Go Reader.readHeader: every numeric field of the format must parse, even + * those the tree does not use, or the header is malformed. + */ export function parseHeader(block: Uint8Array): RawHeader { if (!checksumMatches(block)) throw readFailed("tar header checksum mismatch"); - let name = cString(block, 0, 100); - const ustar = startsWith(block, 257, "ustar\u000000"); - const gnu = startsWith(block, 257, "ustar \u0000"); - if (ustar) { - // star archives end the header with "tar\0" and keep a shorter prefix. - const star = startsWith(block, 508, "tar\u0000"); - const prefix = cString(block, 345, star ? 131 : 155); - if (prefix.length > 0) { - const joined = new Uint8Array(prefix.length + 1 + name.length); - joined.set(prefix); - joined[prefix.length] = 0x2f; - joined.set(name, prefix.length + 1); - name = joined; + const format = formatOf(block); + const field = (start: number, length: number) => block.subarray(start, start + length); + const size = parseNumeric(field(124, 12), "size"); + const mode = parseNumeric(field(100, 8), "mode"); + parseNumeric(field(108, 8), "uid"); + parseNumeric(field(116, 8), "gid"); + parseNumeric(field(136, 12), "mtime"); + let prefix: Uint8Array = new Uint8Array(0); + if (format !== "v7") { + parseNumeric(field(329, 8), "devmajor"); + parseNumeric(field(337, 8), "devminor"); + } + if (format === "ustar") prefix = cString(field(345, 155)); + if (format === "star") { + prefix = cString(field(345, 131)); + parseNumeric(field(476, 12), "atime"); + parseNumeric(field(488, 12), "ctime"); + } + if (format === "gnu") { + // Pre-Go 1.8 writers put a USTAR prefix where GNU keeps atime and ctime; + // Go falls back to it when those fields do not parse and it is ASCII. + try { + if (block[345] !== 0) parseNumeric(field(345, 12), "atime"); + if (block[357] !== 0) parseNumeric(field(357, 12), "ctime"); + } catch { + const legacy = cString(field(345, 155)); + if (legacy.every((b) => b < 0x80)) prefix = legacy; } } - const typeflag = String.fromCharCode(block[156]!); return { - name, - linkname: cString(block, 157, 100), - mode: parseNumeric(block.subarray(100, 108), "mode"), - size: parseNumeric(block.subarray(124, 136), "size"), - typeflag, - gnuExtended: gnu && typeflag === "S" && block[482] !== 0, + name: joinPrefix(prefix, cString(field(0, 100))), + linkname: cString(field(157, 100)), + mode: Number(mode), + size: Number(size), + typeflag: String.fromCharCode(block[156]!), + format, + block, }; } const utf8 = new TextDecoder("utf-8"); +export const SPARSE_MAP = "GNU.sparse.map"; +const SPARSE_OFFSET = "GNU.sparse.offset"; +const SPARSE_NUMBYTES = "GNU.sparse.numbytes"; +/** Records whose raw value bytes are kept, since they become entry names. */ +const RAW_RECORDS = new Set(["path", "GNU.sparse.name"]); + export interface PaxRecords { + /** Records by key, in a prototype-free object so no key can reach Object.prototype. */ records: Record; - /** Raw bytes of the `path` record, which may not be valid UTF-8. */ - rawPath?: Uint8Array; + /** Raw value bytes of `path` and `GNU.sparse.name`, which may not be valid UTF-8. */ + raw: Map; +} + +/** Go parsePAXRecord's length: ParseInt base 10, so a sign and leading zeros are allowed. */ +function recordLength(text: string): number { + return /^[+-]?[0-9]+$/.test(text) ? Number(text) : NaN; } -/** Parses PAX records " =\n" like Go's parsePAX; later records win. */ +/** + * Parses PAX records " =\n" like Go's parsePAX; later + * records win, and GNU 0.0 sparse offset/numbytes pairs become `GNU.sparse.map`. + */ export function parsePax(data: Uint8Array): PaxRecords { - const records: Record = {}; - let rawPath: Uint8Array | undefined; + const records: Record = Object.create(null) as Record; + const raw = new Map(); + const sparseMap: string[] = []; let pos = 0; while (pos < data.length) { const space = data.indexOf(0x20, pos); - const lenText = space < 0 ? "" : latin.decode(data.subarray(pos, space)); - if (!/^[1-9][0-9]*$/.test(lenText)) throw readFailed("invalid PAX record length"); - const end = pos + Number(lenText); - if (end > data.length || end <= space + 1 || data[end - 1] !== 0x0a) throw readFailed("invalid PAX record"); + if (space < 0) throw readFailed("invalid PAX record length"); + const lenText = byteString(data.subarray(pos, space)); + const n = recordLength(lenText); + if (!(n >= 5 && n <= data.length - pos) || n - (lenText.length + 1) <= 0) throw readFailed("invalid PAX record length"); + const end = pos + n; + if (data[end - 1] !== 0x0a) throw readFailed("invalid PAX record"); const body = data.subarray(space + 1, end - 1); const eq = body.indexOf(0x3d); if (eq <= 0) throw readFailed("invalid PAX record key"); - const key = utf8.decode(body.subarray(0, eq)); - const value = utf8.decode(body.subarray(eq + 1)); - const nulInValue = ["path", "linkpath", "uname", "gname"].includes(key) && value.includes("\0"); - if (key.includes("\0") || nulInValue) throw readFailed(`invalid PAX record ${JSON.stringify(key)}`); - records[key] = value; - if (key === "path") rawPath = body.subarray(eq + 1); + const keyBytes = body.subarray(0, eq); + const valueBytes = body.subarray(eq + 1); + const key = utf8.decode(keyBytes); + const value = utf8.decode(valueBytes); + const nulInValue = ["path", "linkpath", "uname", "gname"].includes(key) ? valueBytes.includes(0) : keyBytes.includes(0); + if (nulInValue) throw readFailed(`invalid PAX record ${JSON.stringify(key)}`); pos = end; + if (key === SPARSE_OFFSET || key === SPARSE_NUMBYTES) { + const want = sparseMap.length % 2 === 0 ? SPARSE_OFFSET : SPARSE_NUMBYTES; + if (key !== want || value.includes(",")) throw readFailed("invalid GNU sparse PAX records"); + sparseMap.push(value); + continue; + } + records[key] = value; + if (RAW_RECORDS.has(key)) raw.set(key, valueBytes); } - return rawPath === undefined ? { records } : { records, rawPath }; + if (sparseMap.length > 0) records[SPARSE_MAP] = sparseMap.join(","); + return { records, raw }; +} + +/** Go strconv.ParseInt(value, 10, 64), or undefined when it fails. */ +export function parseInt64(value: string): bigint | undefined { + if (!/^[+-]?[0-9]+$/.test(value)) return undefined; + const n = BigInt(value); + return n < INT64_MIN || n > INT64_MAX ? undefined : n; } -const DECIMAL_RECORDS = ["size", "uid", "gid"]; -const TIME_RECORDS = ["atime", "mtime", "ctime"]; +/** Go parsePAXTime: int64 seconds, then optional "." and sub-second digits ("1." is valid). */ +function validPaxTime(value: string): boolean { + const dot = value.indexOf("."); + const secs = dot < 0 ? value : value.slice(0, dot); + return parseInt64(secs) !== undefined && /^[0-9]*$/.test(dot < 0 ? "" : value.slice(dot + 1)); +} -/** Rejects records Go's mergePAX cannot parse. */ -export function checkPaxValues(records: Readonly>): void { - for (const [key, value] of Object.entries(records)) { - if (DECIMAL_RECORDS.includes(key) && !(/^[0-9]+$/.test(value) && Number.isSafeInteger(Number(value)))) { - throw readFailed(`invalid PAX ${key}`); +const INT_RECORDS = new Set(["size", "uid", "gid"]); +const TIME_RECORDS = new Set(["atime", "mtime", "ctime"]); + +/** + * Go mergePAX: rejects a non-empty value it cannot parse, and returns the + * `size` record when it overrides the header (empty values keep the header's). + */ +export function mergePaxValues(records: Readonly>): number | undefined { + let size: number | undefined; + for (const key of Object.keys(records)) { + const value = records[key]!; + if (value === "") continue; + if (INT_RECORDS.has(key)) { + const n = parseInt64(value); + if (n === undefined) throw readFailed(`invalid PAX ${key}`); + if (key === "size") size = Number(n); } - if (TIME_RECORDS.includes(key) && !/^-?[0-9]+(\.[0-9]+)?$/.test(value)) throw readFailed(`invalid PAX ${key}`); + if (TIME_RECORDS.has(key) && !validPaxTime(value)) throw readFailed(`invalid PAX ${key}`); } + return size; } /** Header-only types carry no data regardless of their size field (Go isHeaderOnlyType). */ diff --git a/packages/skill-tree/src/tar-reader.ts b/packages/skill-tree/src/tar-reader.ts index eca49f6..551ee72 100644 --- a/packages/skill-tree/src/tar-reader.ts +++ b/packages/skill-tree/src/tar-reader.ts @@ -12,13 +12,14 @@ import { BLOCK, MAX_SPECIAL_FILE_SIZE, blockPadding, - checkPaxValues, isHeaderOnly, isZeroBlock, + mergePaxValues, parseHeader, parsePax, } from "./tar-header"; import type { PaxRecords } from "./tar-header"; +import { readOldGnuSparse, readPaxSparse } from "./tar-sparse"; import { LIMITS, headerMode } from "./tree"; import type { EntryKind, TreeLimits } from "./tree"; @@ -83,6 +84,25 @@ async function readSpecial(reader: StreamReader, size: number): Promise= 0)) throw readFailed(`tar entry has a negative size ${size}`); + return n; +} + +/** Bytes an archive may take beyond its file content: headers, extension records and skipped data. */ +const ENTRY_OVERHEAD = 16 * 1024; + +/** + * The total byte budget of one archive. Go archive/tar has no such limit; + * it bounds data that no file limit counts (non-regular entry data, PAX and + * GNU extension chains), so a stream cannot be read without end. + */ +export function tarByteBudget(limits: TreeLimits): number { + return limits.maxTreeSize + limits.maxEntries * ENTRY_OVERHEAD; +} + async function readEntries(reader: StreamReader, collector: EntryCollector): Promise { let pax: PaxRecords | undefined; let longName: Uint8Array | undefined; @@ -96,44 +116,62 @@ async function readEntries(reader: StreamReader, collector: EntryCollector): Pro } const h = parseHeader(block); if (h.typeflag === "x") { - pax = parsePax(await readSpecial(reader, h.size)); + pax = parsePax(await readSpecial(reader, dataLength(h.typeflag, h.size))); continue; } if (h.typeflag === "L" || h.typeflag === "K") { - const value = nulTerminated(await readSpecial(reader, h.size)); - if (h.typeflag === "L" && value.length > 0) longName = value; + const value = nulTerminated(await readSpecial(reader, dataLength(h.typeflag, h.size))); + // An empty long name clears an earlier one, as Go's parseString result does. + if (h.typeflag === "L") longName = value.length > 0 ? value : undefined; continue; } collector.present(); if (h.typeflag === "g") { // Global records describe the archive; pending x and L headers are dropped, as in Go. - const global = parsePax(await readSpecial(reader, h.size)); - checkPaxValues(global.records); - const name = decodeName(global.rawPath ?? h.name); - collector.add({ path: name.path, utf8Valid: name.utf8Valid, mode: "", size: 0, sha256: "", kind: "metadata", paxRecords: global.records }); + // Go ignores a global header's merge failure and then reports no records. + const global = parsePax(await readSpecial(reader, dataLength(h.typeflag, h.size))); + let records = global.records; + try { + mergePaxValues(records); + } catch { + records = Object.create(null) as Record; + } + const name = decodeName(records.path ? global.raw.get("path")! : h.name); + collector.add({ path: name.path, utf8Valid: name.utf8Valid, mode: "", size: 0, sha256: "", kind: "metadata", paxRecords: records }); pax = undefined; longName = undefined; continue; } - const records = pax?.records ?? {}; - checkPaxValues(records); - const name = decodeName(longName ?? pax?.rawPath ?? h.name); - const declared = records.size !== undefined ? Number(records.size) : h.size; - const dataSize = isHeaderOnly(h.typeflag) ? 0 : declared; + const records = pax?.records ?? (Object.create(null) as Record); + const size = mergePaxValues(records) ?? h.size; + let rawName = longName ?? (records.path ? pax!.raw.get("path")! : h.name); + const physical = dataLength(h.typeflag, size); + let logical = size; + let consumed = 0; + if (h.typeflag === "S") await readOldGnuSparse(reader, h); + else { + const sparse = await readPaxSparse(reader, h, pax, size, physical); + if (sparse !== undefined) { + rawName = sparse.name ?? rawName; + logical = sparse.size; + consumed = sparse.consumed; + } + } + const name = decodeName(rawName); const kind = tarEntryKind(h.typeflag, name.path); - if (h.gnuExtended) { - // Old GNU sparse headers chain extension blocks before the data. - for (let ext = await reader.readExact(BLOCK); ; ext = await reader.readExact(BLOCK)) { - if (ext === null) throw readFailed("truncated GNU sparse header"); - if (ext[504] === 0) break; + const sparse = logical !== size || consumed > 0 || Object.keys(records).some((k) => k.startsWith("GNU.sparse.")); + let hashed = { size: 0, sha256: "" }; + if (kind === "regular" && !sparse) hashed = await collector.hashFile(name.path, reader.chunks(physical)); + else { + // Sparse files are rejected later, but their logical size counts toward the limits first. + if (kind === "regular") { + collector.countFile(name.path, logical); + hashed = { size: logical, sha256: "" }; } + await reader.skip(physical - consumed); } - let size = 0; - let sha256 = ""; - if (kind === "regular") ({ size, sha256 } = await collector.hashFile(name.path, reader.chunks(dataSize))); - else await reader.skip(dataSize); - await reader.skip(blockPadding(dataSize)); - collector.add({ path: name.path, utf8Valid: name.utf8Valid, mode: headerMode(h.mode), size, sha256, kind, paxRecords: records }); + await reader.skip(blockPadding(physical)); + collector.add({ path: name.path, utf8Valid: name.utf8Valid, mode: headerMode(h.mode), ...hashed, kind, paxRecords: records }); pax = undefined; longName = undefined; } @@ -145,10 +183,11 @@ async function readEntries(reader: StreamReader, collector: EntryCollector): Pro * limits stop reading as soon as they are exceeded. */ export async function readTarTree(source: ByteSource, options: TarReadOptions = {}): Promise { - const collector = new EntryCollector(options.limits ?? LIMITS); + const limits = options.limits ?? LIMITS; + const collector = new EntryCollector(limits); let reader: StreamReader | undefined; try { - reader = new StreamReader(await gunzipIfNeeded(toAsyncIterable(source), options.compression ?? "auto")); + reader = new StreamReader(await gunzipIfNeeded(toAsyncIterable(source), options.compression ?? "auto"), tarByteBudget(limits)); await readEntries(reader, collector); } catch (err) { throw asTreeError(err); diff --git a/packages/skill-tree/src/tar-sparse.ts b/packages/skill-tree/src/tar-sparse.ts new file mode 100644 index 0000000..24ad96f --- /dev/null +++ b/packages/skill-tree/src/tar-sparse.ts @@ -0,0 +1,167 @@ +/** + * GNU sparse maps as Go archive/tar Reader.Next reads them. The tree never + * accepts a sparse file (contract 4.4), but the map decides the entry's name, + * logical size and how much of its data section is the map itself, and a + * malformed map is a malformed archive (`tree.read_failed`), as in Go. + */ +import { readFailed } from "./byte-source"; +import type { StreamReader } from "./byte-source"; +import { BLOCK, MAX_SPECIAL_FILE_SIZE, SPARSE_MAP, byteString, isHeaderOnly, parseInt64, parseNumeric } from "./tar-header"; +import type { PaxRecords, RawHeader } from "./tar-header"; + +interface SparseEntry { + offset: bigint; + length: bigint; +} + +const INT64_MAX = (1n << 63n) - 1n; +/** Go parses the entry count as a native int and rejects counts whose double overflows. */ +const MAX_SPARSE_COUNT = INT64_MAX / 2n; + +/** Go validateSparseEntries: in order, non-overlapping, non-negative and within `size`. */ +function validSparseEntries(entries: readonly SparseEntry[], size: bigint): boolean { + if (size < 0n) return false; + let preEnd = 0n; + for (const { offset, length } of entries) { + if (offset < 0n || length < 0n || offset > INT64_MAX - length) return false; + if (offset + length > size || preEnd > offset) return false; + preEnd = offset + length; + } + return true; +} + +function sparseCount(text: string | undefined): bigint { + const n = parseInt64(text ?? ""); + if (n === undefined || n < 0n || n > MAX_SPARSE_COUNT) throw readFailed("invalid GNU sparse entry count"); + return n; +} + +function sparsePair(offset: string, length: string): SparseEntry { + const o = parseInt64(offset); + const l = parseInt64(length); + if (o === undefined || l === undefined) throw readFailed("invalid GNU sparse map entry"); + return { offset: o, length: l }; +} + +/** Go readGNUSparseMap0x1: the map from the GNU.sparse.numblocks and GNU.sparse.map records. */ +function map0x1(records: Readonly>): SparseEntry[] { + const n = sparseCount(records["GNU.sparse.numblocks"]); + const map = records[SPARSE_MAP] ?? ""; + const fields = map === "" ? [] : map.split(","); + if (BigInt(fields.length) !== 2n * n) throw readFailed("GNU sparse map does not match its entry count"); + const out: SparseEntry[] = []; + for (let i = 0; i + 1 < fields.length; i += 2) out.push(sparsePair(fields[i]!, fields[i + 1]!)); + return out; +} + +/** + * Go readGNUSparseMap1x0: newline-separated decimal fields at the start of + * the data section, read in whole blocks up to 1 MiB. Returns the map and + * the data bytes it took. + */ +async function map1x0(reader: StreamReader, available: number): Promise<{ entries: SparseEntry[]; consumed: number }> { + let text = ""; + let newlines = 0; + let consumed = 0; + let at = 0; + const feed = async (want: bigint) => { + while (BigInt(newlines) < want) { + consumed += BLOCK; + if (consumed > MAX_SPECIAL_FILE_SIZE) throw readFailed("GNU sparse map is too long"); + const block = consumed <= available ? await reader.readExact(BLOCK) : null; + if (block === null) throw readFailed("truncated GNU sparse map"); + const chunk = byteString(block); + text += chunk; + for (let i = 0; i < chunk.length; i++) if (chunk.charCodeAt(i) === 0x0a) newlines++; + } + }; + const token = () => { + const nl = text.indexOf("\n", at); + const out = text.slice(at, nl); + at = nl + 1; + newlines--; + return out; + }; + await feed(1n); + const n = sparseCount(token()); + await feed(2n * n); + const entries: SparseEntry[] = []; + for (let i = 0n; i < n; i++) entries.push(sparsePair(token(), token())); + return { entries, consumed }; +} + +/** Go readGNUSparsePAXHeaders version detection: "0.x", "1.0", or undefined when not sparse. */ +function paxSparseVersion(records: Readonly>): "0.x" | "1.0" | undefined { + const major = records["GNU.sparse.major"] ?? ""; + const minor = records["GNU.sparse.minor"] ?? ""; + if (major === "0" && (minor === "0" || minor === "1")) return "0.x"; + if (major === "1" && minor === "0") return "1.0"; + if (major !== "" || minor !== "") return undefined; // an unknown version is not sparse + return (records[SPARSE_MAP] ?? "") !== "" ? "0.x" : undefined; +} + +export interface PaxSparse { + /** `GNU.sparse.name`, which replaces the entry name. */ + name?: Uint8Array; + /** Logical file size. */ + size: number; + /** Data section bytes the 1.0 map occupied. */ + consumed: number; +} + +/** + * Applies GNU sparse PAX records like Go: the name and size overrides, then + * the map, validated against the logical size. Returns undefined when the + * records do not describe a sparse file. + */ +export async function readPaxSparse( + reader: StreamReader, + h: RawHeader, + pax: PaxRecords | undefined, + size: number, + physical: number, +): Promise { + const records = pax?.records ?? {}; + const version = paxSparseVersion(records); + if (version === undefined) return undefined; + const nameText = records["GNU.sparse.name"] ?? ""; + const sizeText = records["GNU.sparse.size"] || records["GNU.sparse.realsize"] || ""; + let logical = BigInt(size); + if (sizeText !== "") { + const n = parseInt64(sizeText); + if (n === undefined) throw readFailed("invalid GNU sparse size"); + logical = n; + } + const { entries, consumed } = version === "1.0" ? await map1x0(reader, physical) : { entries: map0x1(records), consumed: 0 }; + if (isHeaderOnly(h.typeflag) || !validSparseEntries(entries, logical)) throw readFailed("invalid GNU sparse map"); + return { ...(nameText === "" ? {} : { name: pax?.raw.get("GNU.sparse.name") }), size: Number(logical), consumed }; +} + +/** + * Go readOldGNUSparseMap for type 'S': GNU format only, map entries in the + * header and in extension blocks that precede the data. + */ +export async function readOldGnuSparse(reader: StreamReader, h: RawHeader): Promise { + if (h.format !== "gnu") throw readFailed("old GNU sparse header outside the GNU format"); + let block = h.block; + const size = parseNumeric(block.subarray(483, 495), "sparse real size"); + const entries: SparseEntry[] = []; + let base = 386; + let count = 4; + let extended = 482; + for (;;) { + for (let i = 0; i < count; i++) { + const at = base + i * 24; + if (block[at] === 0) break; + entries.push({ offset: parseNumeric(block.subarray(at, at + 12), "sparse offset"), length: parseNumeric(block.subarray(at + 12, at + 24), "sparse length") }); + } + if (block[extended]! === 0) break; + const next = await reader.readExact(BLOCK); + if (next === null) throw readFailed("truncated GNU sparse header"); + block = next; + base = 0; + count = 21; + extended = 504; + } + if (!validSparseEntries(entries, size)) throw readFailed("invalid GNU sparse map"); +} diff --git a/packages/skill-tree/src/tree.ts b/packages/skill-tree/src/tree.ts index 961e171..f8fd0b6 100644 --- a/packages/skill-tree/src/tree.ts +++ b/packages/skill-tree/src/tree.ts @@ -69,12 +69,24 @@ function isNoiseEntry(e: TreeEntry): boolean { } } -/** Maps a tar or zip header mode to its class; any other mode keeps its octal form and is rejected. */ +/** The largest mode a tar header's 8-byte octal field can hold. */ +const MAX_HEADER_MODE = 0o7777777; +const TYPE_BITS = 0o170000; + +/** + * Maps a tar or zip header mode to its class; any other mode keeps its + * octal form and is rejected, like Go's HeaderMode over int64. A mode that is + * not a safe integer, negative or wider than the octal field is rejected + * before the type bits are cleared, so no bit above them is ever dropped + * (JavaScript bitwise operators truncate to 32 bits). + */ export function headerMode(mode: number): string { - const perm = mode & ~0o170000; - if (perm === 0o644) return MODE_REGULAR; - if (perm === 0o755) return MODE_EXECUTABLE; - return `0${mode.toString(8)}`; + if (Number.isSafeInteger(mode) && mode >= 0 && mode <= MAX_HEADER_MODE) { + const perm = mode & ~TYPE_BITS; + if (perm === 0o644) return MODE_REGULAR; + if (perm === 0o755) return MODE_EXECUTABLE; + } + return Number.isFinite(mode) ? `0${mode.toString(8)}` : `0${mode}`; } function isSparseRecord(key: string): boolean { @@ -112,7 +124,11 @@ export function checkLimits(entries: readonly TreeEntry[], listed: number, limit for (const e of entries) { if (kindOf(e) !== "regular") continue; if (++count > limits.maxFiles) return treeError(ERROR_CODES.limitExceeded, e.path, `tree has more than ${limits.maxFiles} files`); - const size = Math.max(e.size, 0); + // A negative size counts as none (validation rejects it later); any other + // size that is not a safe integer (NaN, fractional, infinite, beyond 2^53) + // cannot be summed exactly and fails here. + const size = e.size < 0 ? 0 : e.size; + if (!Number.isSafeInteger(size)) return treeError(ERROR_CODES.limitExceeded, e.path, `file size ${e.size} is not a safe integer`); if (size > limits.maxFileSize) return treeError(ERROR_CODES.limitExceeded, e.path, `file exceeds ${limits.maxFileSize} bytes`); if ((total += size) > limits.maxTreeSize) return treeError(ERROR_CODES.limitExceeded, e.path, `tree exceeds ${limits.maxTreeSize} bytes`); } diff --git a/packages/skill-tree/src/unicode-15-tables.ts b/packages/skill-tree/src/unicode-15-tables.ts index 7936df5..9e2940e 100644 --- a/packages/skill-tree/src/unicode-15-tables.ts +++ b/packages/skill-tree/src/unicode-15-tables.ts @@ -1,7 +1,7 @@ // Code generated by scripts/gen-unicode-tables.mjs. DO NOT EDIT. // Unicode 15.0.0. Inputs: // fixtures/ucd/CaseFolding-15.0.0.txt sha256 cdd49e55eae3bbf1f0a3f6580c974a0263cb86a6a08daa10fbf705b4808a56f7 -// scripts/unicode-15.0.0-go.json sha256 baa335155dcbaf0cfe8f40fcc0a98232c4134e821a08c3f198f829a51d911ad5 (go1.25.3, x/text norm 15.0.0) +// scripts/unicode-15.0.0-go.json sha256 0d0155948ceea434fa241b59937db1a41689fa2ccb8231d35d96a2c56f57b52b (go1.25.3, x/text norm 15.0.0) // Encodings: ranges are "delta.length" base-36 items (delta from the previous // range end); maps are "keyDelta:value.value"; lists are base-36 deltas. @@ -12,3 +12,4 @@ export const COMBINING_CLASSES = "lc.k.6e,1.0.6g,1.3.64,1.0.6g,1.0.60,1.4.64,1.1 export const CANONICAL_DECOMPOSITIONS = "5c:1t.lc,1:1t.ld,1:1t.le,1:1t.lf,1:1t.lk,1:1t.lm,2:1v.mf,1:1x.lc,1:1x.ld,1:1x.le,1:1x.lk,1:21.lc,1:21.ld,1:21.le,1:21.lk,2:26.lf,1:27.lc,1:27.ld,1:27.le,1:27.lf,1:27.lk,3:2d.lc,1:2d.ld,1:2d.le,1:2d.lk,1:2h.ld,3:2p.lc,1:2p.ld,1:2p.le,1:2p.lf,1:2p.lk,1:2p.lm,2:2r.mf,1:2t.lc,1:2t.ld,1:2t.le,1:2t.lk,1:2x.lc,1:2x.ld,1:2x.le,1:2x.lk,2:32.lf,1:33.lc,1:33.ld,1:33.le,1:33.lf,1:33.lk,3:39.lc,1:39.ld,1:39.le,1:39.lk,1:3d.ld,2:3d.lk,1:1t.lg,1:2p.lg,1:1t.li,1:2p.li,1:1t.mg,1:2p.mg,1:1v.ld,1:2r.ld,1:1v.le,1:2r.le,1:1v.lj,1:2r.lj,1:1v.lo,1:2r.lo,1:1w.lo,1:2s.lo,3:1x.lg,1:2t.lg,1:1x.li,1:2t.li,1:1x.lj,1:2t.lj,1:1x.mg,1:2t.mg,1:1x.lo,1:2t.lo,1:1z.le,1:2v.le,1:1z.li,1:2v.li,1:1z.lj,1:2v.lj,1:1z.mf,1:2v.mf,1:20.le,1:2w.le,3:21.lf,1:2x.lf,1:21.lg,1:2x.lg,1:21.li,1:2x.li,1:21.mg,1:2x.mg,1:21.lj,4:22.le,1:2y.le,1:23.mf,1:2z.mf,2:24.ld,1:30.ld,1:24.mf,1:30.mf,1:24.lo,1:30.lo,5:26.ld,1:32.ld,1:26.mf,1:32.mf,1:26.lo,1:32.lo,4:27.lg,1:33.lg,1:27.li,1:33.li,1:27.ln,1:33.ln,3:2a.ld,1:36.ld,1:2a.mf,1:36.mf,1:2a.lo,1:36.lo,1:2b.ld,1:37.ld,1:2b.le,1:37.le,1:2b.mf,1:37.mf,1:2b.lo,1:37.lo,1:2c.mf,1:38.mf,1:2c.lo,1:38.lo,3:2d.lf,1:39.lf,1:2d.lg,1:39.lg,1:2d.li,1:39.li,1:2d.lm,1:39.lm,1:2d.ln,1:39.ln,1:2d.mg,1:39.mg,1:2f.le,1:3b.le,1:2h.le,1:3d.le,1:2h.lk,1:2i.ld,1:3e.ld,1:2i.lj,1:3e.lj,1:2i.lo,1:3e.lo,y:27.m3,1:33.m3,e:2d.m3,1:39.m3,t:1t.lo,1:2p.lo,1:21.lo,1:2x.lo,1:27.lo,1:33.lo,1:2d.lo,1:39.lo,1:64.lg,1:70.lg,1:64.ld,1:70.ld,1:64.lo,1:70.lo,1:64.lc,1:70.lc,2:5g.lg,1:6c.lg,1:fa.lg,1:fb.lg,1:5i.lg,1:6e.lg,3:1z.lo,1:2v.lo,1:23.lo,1:2z.lo,1:27.mg,1:33.mg,1:dm.lg,1:dn.lg,1:c7.lo,1:ia.lo,1:2y.lo,4:1z.ld,1:2v.ld,3:26.lc,1:32.lc,1:5h.ld,1:6d.ld,1:5i.ld,1:6e.ld,1:60.ld,1:6w.ld,1:1t.lr,1:2p.lr,1:1t.lt,1:2p.lt,1:1x.lr,1:2t.lr,1:1x.lt,1:2t.lt,1:21.lr,1:2x.lr,1:21.lt,1:2x.lt,1:27.lr,1:33.lr,1:27.lt,1:33.lt,1:2a.lr,1:36.lr,1:2a.lt,1:36.lt,1:2d.lr,1:39.lr,1:2d.lt,1:39.lt,1:2b.me,1:37.me,1:2c.me,1:38.me,3:20.lo,1:2w.lo,7:1t.lj,1:2p.lj,1:1x.mf,1:2t.mf,1:5y.lg,1:6u.lg,1:5x.lg,1:6t.lg,1:27.lj,1:33.lj,1:fi.lg,1:fj.lg,1:2h.lg,1:3d.lg,7h:lc,1:ld,2:lv,1:lk.ld,1c:jd,a:1n,7:4o.ld,1:pd.ld,1:53,1:ph.ld,1:pj.ld,1:pl.ld,2:pr.ld,2:px.ld,1:q1.ld,1:qy.ld,q:pl.lk,1:px.lk,1:q9.ld,1:qd.ld,1:qf.ld,1:qh.ld,1:qz.ld,q:qh.lk,1:qt.lk,1:qn.ld,1:qt.ld,1:qx.ld,5:r6.ld,1:r6.lk,18:t1.lc,1:t1.lk,2:sz.ld,4:sm.lk,5:t6.ld,1:t4.lc,1:tf.li,b:t4.li,w:u0.li,n:tx.lc,1:tx.lk,2:tv.ld,4:uu.lk,5:u2.ld,1:u0.lc,1:ub.li,o:vo.lr,1:vp.lr,22:t2.li,1:ty.li,e:sw.li,1:ts.li,1:sw.lk,1:ts.lk,3:t1.li,1:tx.li,3:yg.lk,1:yh.lk,1:t2.lk,1:ty.lk,1:t3.lk,1:tz.lk,3:t4.lg,1:u0.lg,1:t4.lk,1:u0.lk,1:ta.lk,1:u6.lk,3:yw.lk,1:yx.lk,1:tp.lk,1:ul.lk,1:tf.lg,1:ub.lg,1:tf.lk,1:ub.lk,1:tf.ln,1:ub.ln,1:tj.lk,1:uf.lk,3:tn.lk,1:uj.lk,89:17r.18z,1:17r.190,1:18o.190,1:17r.191,1:18q.190,4a:1cl.190,2:1c1.190,h:1ci.190,gm:1t4.1to,8:1tc.1to,3:1tf.1to,10:1sl.1to,1:1sm.1to,1:1sn.1to,1:1ss.1to,1:1sx.1to,1:1sy.1to,1:1t7.1to,1:1tb.1to,30:1xj.1xa,1:1xj.1xz,g:1wh.1x8,1:1wi.1x8,2:1wv.1x8,2c:20i.20s,3:20o.20s,z:1zq.20s,1:1zr.20s,1:1zw.20s,3:20b.20s,6i:287.28m,3:287.27y,1:287.28n,g:275.27w,1:276.27w,1j:2aa.2c7,1i:2bq.2bi,1:2br.2bi,1:2bq.2c7,3g:2fa.2fq,3c:2in.2j9,7:2iu.2j9,1:2iu.2ja,2:2iu.2iq,1:2iy.2j9,3j:2me.2m6,1:2mf.2m6,1:2me.2mv,3y:2qh.2q2,2:2qh.2q7,1:2qk.2q2,1:2qh.2qn,9x:30i.33r,a:30s.33r,5:30x.33r,5:312.33r,5:317.33r,d:30g.33p,a:31t.31u,2:31t.31w,1:33m.328,2:33n.328,9:31t.328,i:32q.33r,a:330.33r,5:335.33r,5:33a.33r,5:33f.33r,d:32o.33p,31:36t.372,25c:5c5.5dh,2:5c7.5dh,2:5c9.5dh,2:5cb.5dh,2:5cd.5dh,4:5ch.5dh,15:5dm.5dh,2:5do.5dh,3:5dq.5dh,1:5dr.5dh,2:5du.5dh,jh:1t.md,1:2p.md,1:1u.lj,1:2q.lj,1:1u.mb,1:2q.mb,1:1u.mp,1:2q.mp,1:5j.ld,1:6f.ld,1:1w.lj,1:2s.lj,1:1w.mb,1:2s.mb,1:1w.mp,1:2s.mp,1:1w.mf,1:2s.mf,1:1w.ml,1:2s.ml,1:7m.lc,1:7n.lc,1:7m.ld,1:7n.ld,1:1x.ml,1:2t.ml,1:1x.mo,1:2t.mo,1:fc.li,1:fd.li,1:1y.lj,1:2u.lj,1:1z.lg,1:2v.lg,1:20.lj,1:2w.lj,1:20.mb,1:2w.mb,1:20.lk,1:2w.lk,1:20.mf,1:2w.mf,1:20.mm,1:2w.mm,1:21.mo,1:2x.mo,1:5r.ld,1:6n.ld,1:23.ld,1:2z.ld,1:23.mb,1:2z.mb,1:23.mp,1:2z.mp,1:24.mb,1:30.mb,1:5yu.lg,1:5yv.lg,1:24.mp,1:30.mp,1:24.ml,1:30.ml,1:25.ld,1:31.ld,1:25.lj,1:31.lj,1:25.mb,1:31.mb,1:26.lj,1:32.lj,1:26.mb,1:32.mb,1:26.mp,1:32.mp,1:26.ml,1:32.ml,1:5x.ld,1:6t.ld,1:5x.lk,1:6t.lk,1:98.lc,1:99.lc,1:98.ld,1:99.ld,1:28.ld,1:34.ld,1:28.lj,1:34.lj,1:2a.lj,1:36.lj,1:2a.mb,1:36.mb,1:5zu.lg,1:5zv.lg,1:2a.mp,1:36.mp,1:2b.lj,1:37.lj,1:2b.mb,1:37.mb,1:9m.lj,1:9n.lj,1:9s.lj,1:9t.lj,1:602.lj,1:603.lj,1:2c.lj,1:38.lj,1:2c.mb,1:38.mb,1:2c.mp,1:38.mp,1:2c.ml,1:38.ml,1:2d.mc,1:39.mc,1:2d.mo,1:39.mo,1:2d.ml,1:39.ml,1:a0.ld,1:a1.ld,1:a2.lk,1:a3.lk,1:2e.lf,1:3a.lf,1:2e.mb,1:3a.mb,1:2f.lc,1:3b.lc,1:2f.ld,1:3b.ld,1:2f.lk,1:3b.lk,1:2f.lj,1:3b.lj,1:2f.mb,1:3b.mb,1:2g.lj,1:3c.lj,1:2g.lk,1:3c.lk,1:2h.lj,1:3d.lj,1:2i.le,1:3e.le,1:2i.mb,1:3e.mb,1:2i.mp,1:3e.mp,1:2w.mp,1:38.lk,1:3b.lm,1:3d.lm,2:an.lj,5:1t.mb,1:2p.mb,1:1t.ll,1:2p.ll,1:5e.ld,1:6a.ld,1:5e.lc,1:6a.lc,1:5e.ll,1:6a.ll,1:5e.lf,1:6a.lf,1:61s.le,1:61t.le,1:76.ld,1:77.ld,1:76.lc,1:77.lc,1:76.ll,1:77.ll,1:76.lf,1:77.lf,1:61s.li,1:61t.li,1:1x.mb,1:2t.mb,1:1x.ll,1:2t.ll,1:1x.lf,1:2t.lf,1:5m.ld,1:6i.ld,1:5m.lc,1:6i.lc,1:5m.ll,1:6i.ll,1:5m.lf,1:6i.lf,1:62g.le,1:62h.le,1:21.ll,1:2x.ll,1:21.mb,1:2x.mb,1:27.mb,1:33.mb,1:27.ll,1:33.ll,1:5w.ld,1:6s.ld,1:5w.lc,1:6s.lc,1:5w.ll,1:6s.ll,1:5w.lf,1:6s.lf,1:630.le,1:631.le,1:bk.ld,1:bl.ld,1:bk.lc,1:bl.lc,1:bk.ll,1:bl.ll,1:bk.lf,1:bl.lf,1:bk.mb,1:bl.mb,1:2d.mb,1:39.mb,1:2d.ll,1:39.ll,1:bz.ld,1:c0.ld,1:bz.lc,1:c0.lc,1:bz.ll,1:c0.ll,1:bz.lf,1:c0.lf,1:bz.mb,1:c0.mb,1:2h.lc,1:3d.lc,1:2h.mb,1:3d.mb,1:2h.ll,1:3d.ll,1:2h.lf,1:3d.lf,7:q9.lv,1:q9.lw,1:64g.lc,1:64h.lc,1:64g.ld,1:64h.ld,1:64g.n6,1:64h.n6,1:pd.lv,1:pd.lw,1:64o.lc,1:64p.lc,1:64o.ld,1:64p.ld,1:64o.n6,1:64p.n6,1:qd.lv,1:qd.lw,1:64w.lc,1:64x.lc,1:64w.ld,1:64x.ld,3:ph.lv,1:ph.lw,1:654.lc,1:655.lc,1:654.ld,1:655.ld,3:qf.lv,1:qf.lw,1:65c.lc,1:65d.lc,1:65c.ld,1:65d.ld,1:65c.n6,1:65d.n6,1:pj.lv,1:pj.lw,1:65k.lc,1:65l.lc,1:65k.ld,1:65l.ld,1:65k.n6,1:65l.n6,1:qh.lv,1:qh.lw,1:65s.lc,1:65t.lc,1:65s.ld,1:65t.ld,1:65s.n6,1:65t.n6,1:pl.lv,1:pl.lw,1:660.lc,1:661.lc,1:660.ld,1:661.ld,1:660.n6,1:661.n6,1:qn.lv,1:qn.lw,1:668.lc,1:669.lc,1:668.ld,1:669.ld,3:pr.lv,1:pr.lw,1:66g.lc,1:66h.lc,1:66g.ld,1:66h.ld,3:qt.lv,1:qt.lw,1:66o.lc,1:66p.lc,1:66o.ld,1:66p.ld,1:66o.n6,1:66p.n6,2:px.lw,2:66x.lc,2:66x.ld,2:66x.n6,1:qx.lv,1:qx.lw,1:674.lc,1:675.lc,1:674.ld,1:675.ld,1:674.n6,1:675.n6,1:q1.lv,1:q1.lw,1:67c.lc,1:67d.lc,1:67c.ld,1:67d.ld,1:67c.n6,1:67d.n6,1:q9.lc,1:q9.ld,1:qd.lc,1:qd.ld,1:qf.lc,1:qf.ld,1:qh.lc,1:qh.ld,1:qn.lc,1:qn.ld,1:qt.lc,1:qt.ld,1:qx.lc,1:qx.ld,3:64g.n9,1:64h.n9,1:64i.n9,1:64j.n9,1:64k.n9,1:64l.n9,1:64m.n9,1:64n.n9,1:64o.n9,1:64p.n9,1:64q.n9,1:64r.n9,1:64s.n9,1:64t.n9,1:64u.n9,1:64v.n9,1:65c.n9,1:65d.n9,1:65e.n9,1:65f.n9,1:65g.n9,1:65h.n9,1:65i.n9,1:65j.n9,1:65k.n9,1:65l.n9,1:65m.n9,1:65n.n9,1:65o.n9,1:65p.n9,1:65q.n9,1:65r.n9,1:674.n9,1:675.n9,1:676.n9,1:677.n9,1:678.n9,1:679.n9,1:67a.n9,1:67b.n9,1:67c.n9,1:67d.n9,1:67e.n9,1:67f.n9,1:67g.n9,1:67h.n9,1:67i.n9,1:67j.n9,1:q9.li,1:q9.lg,1:67k.n9,1:q9.n9,1:q4.n9,2:q9.n6,1:69i.n9,1:pd.li,1:pd.lg,1:pd.lc,1:pd.ld,1:pd.n9,2:qh,3:4o.n6,1:67o.n9,1:qf.n9,1:q6.n9,2:qf.n6,1:69y.n9,1:ph.lc,1:ph.ld,1:pj.lc,1:pj.ld,1:pj.n9,1:69r.lc,1:69r.ld,1:69r.n6,1:qh.li,1:qh.lg,1:qy.lc,1:qh.lk.ld,3:qh.n6,1:qy.n6,1:pl.li,1:pl.lg,1:pl.lc,1:pl.ld,2:6bi.lc,1:6bi.ld,1:6bi.n6,1:qt.li,1:qt.lg,1:qz.lc,1:qt.lk.ld,1:qp.lv,1:qp.lw,1:qt.n6,1:qz.n6,1:px.li,1:px.lg,1:px.lc,1:px.ld,1:pt.lw,1:4o.lc,1:4o.ld,1:2o,3:67w.n9,1:qx.n9,1:r2.n9,2:qx.n6,1:6ba.n9,1:pr.lc,1:pr.ld,1:q1.lc,1:q1.ld,1:q1.n9,1:50,3:6bm,1:6bn,85:q1,4:23,1:1t.lm,33:6mo.mw,1:6mq.mw,j:6ms.mw,v:6og.mw,1:6ok.mw,1:6oi.mw,1h:6pv.mw,5:6q0.mw,3:6q3.mw,o:6qr.mw,2:6qt.mw,r:6rg.mw,3:6rn.mw,3:6rp.mw,2:6rs.mw,n:1p.mw,2:6sh.mw,b:6rx.mw,1:1o.mw,1:1q.mw,1:6sk.mw,1:6sl.mw,3:6sy.mw,1:6sz.mw,3:6t2.mw,1:6t3.mw,7:6t6.mw,1:6t7.mw,3:6te.mw,1:6tf.mw,3:6ti.mw,1:6tj.mw,z:6ua.mw,1:6ug.mw,1:6uh.mw,1:6uj.mw,1d:6t8.mw,1:6t9.mw,1:6tt.mw,1:6tu.mw,7:6uq.mw,1:6ur.mw,1:6us.mw,1:6ut.mw,1o:9hk,1:9hl,1iq:8gt.mw,12o:9jf.9ll,2:9jh.9ll,2:9jj.9ll,2:9jl.9ll,2:9jn.9ll,2:9jp.9ll,2:9jr.9ll,2:9jt.9ll,2:9jv.9ll,2:9jx.9ll,2:9jz.9ll,2:9k1.9ll,3:9k4.9ll,2:9k6.9ll,2:9k8.9ll,7:9kf.9ll,1:9kf.9lm,2:9ki.9ll,1:9ki.9lm,2:9kl.9ll,1:9kl.9lm,2:9ko.9ll,1:9ko.9lm,2:9kr.9ll,1:9kr.9lm,n:9ja.9ll,a:9lp.9ll,e:9m3.9ll,2:9m5.9ll,2:9m7.9ll,2:9m9.9ll,2:9mb.9ll,2:9md.9ll,2:9mf.9ll,2:9mh.9ll,2:9mj.9ll,2:9ml.9ll,2:9mn.9ll,2:9mp.9ll,3:9ms.9ll,2:9mu.9ll,2:9mw.9ll,7:9n3.9ll,1:9n3.9lm,2:9n6.9ll,1:9n6.9lm,2:9n9.9ll,1:9n9.9lm,2:9nc.9ll,1:9nc.9lm,2:9nf.9ll,1:9nf.9lm,n:9ly.9ll,3:9nz.9ll,1:9o0.9ll,1:9o1.9ll,1:9o2.9ll,4:9od.9ll,13ia:rpk,1:kc4,1:s7e,1:rt4,1:lw1,1:fg2,1:gkl,1:vj0,1:vj0,1:hn5,1:ssx,1:gw7,1:hmw,1:jcm,1:ne1,1:p6t,1:qin,1:qt6,1:r20,1:sjz,1:kxu,1:ljv,1:mah,1:mvy,1:q4d,1:sq2,1:uep,1:fia,1:ghh,1:l50,1:mjf,1:qi5,1:v8e,1:ifk,1:m3v,1:qfh,1:r50,1:jih,1:pnc,1:qvz,1:iq2,1:kd3,1:lm2,1:mpo,1:slq,1:fpi,1:g53,1:gda,1:jwk,1:l3n,1:mj4,1:nhj,1:pa9,1:qh2,1:qjg,1:s1b,1:tv6,1:upb,1:v7e,1:nt8,1:nzz,1:otc,1:q15,1:t1g,1:vb3,1:rfa,1:hjz,1:iro,1:okg,1:pdq,1:mle,1:nuy,1:rsy,1:ttj,1:hjs,1:i8y,1:kyb,1:lp6,1:lxr,1:oq7,1:oxj,1:tor,1:gcy,1:pe3,1:g64,1:g5o,1:o4c,1:ou6,1:q29,1:tpx,1:rk0,1:jin,1:kxu,1:rge,1:fg9,1:i5j,1:iz6,1:mtj,1:n74,1:gev,1:nwb,1:fr3,1:iw9,1:ff1,1:lho,1:k1k,1:opu,1:gjn,1:hge,1:ni9,1:q4p,1:re2,1:la2,1:sds,1:lfs,1:jjy,1:put,1:jog,1:n6t,1:fji,1:g2x,1:g5l,1:kn5,1:oo7,1:prj,1:rf6,1:ssv,1:gdx,1:gn6,1:ho3,1:ir0,1:k3p,1:m4e,1:nxm,1:tkd,1:ui2,1:vbr,1:vda,1:gbf,1:kau,1:l87,1:sbm,1:ino,1:j9s,1:jcw,1:jve,1:lyb,1:mdl,1:n0p,1:o22,1:ovo,1:pdb,1:s9y,1:q9a,1:sgz,1:t3e,1:g7r,1:gbn,1:gql,1:ma0,1:r0i,1:re2,1:iq1,1:iyd,1:jnf,1:l9q,1:oji,1:mt1,1:fl0,1:h6h,1:i5j,1:iii,1:izg,1:muq,1:mzt,1:p7e,1:pc6,1:svo,1:tti,1:tvs,1:u1k,1:fpn,1:o1a,1:ss4,1:trs,1:j4x,1:fie,1:fyy,1:i5q,1:i7z,1:k2h,1:kxu,1:mha,1:ncy,1:q9o,1:sjg,1:vil,1:k94,1:tny,1:gax,1:kfv,1:kj7,1:lkx,1:luk,1:mx5,1:n6h,1:nsb,1:opc,1:u3i,1:g31,1:je6,1:tq0,1:fu3,1:ie1,1:lpm,1:sa2,1:ivf,1:j7o,1:kk7,1:mtj,1:tqe,1:g89,1:glr,1:i91,1:k5v,1:kem,1:ko8,1:lid,1:mx2,1:naa,1:p6h,1:r0v,1:r1d,1:sss,1:tsy,1:gfz,1:lve,1:gm5,1:mhc,1:n14,1:qgq,1:tr7,1:uvr,1:vbz,1:kgn,1:lor,1:pns,1:o97,1:obk,1:olu,1:mo0,1:m8p,1:riw,1:fk0,1:px2,1:g8q,1:g7b,1:ip2,1:jir,1:onq,1:i2t,1:ljy,1:kac,1:saj,1:qx8,1:tot,1:r63,1:iqb,1:g1s,1:gxs,3:hga,2:k8k,3:g66,1:mqy,1:ngq,1:ny4,1:nz2,1:nz9,1:o0f,1:tw6,1:on2,1:p8d,2:qhe,2:rg8,3:shk,1:sn1,4:u7j,1:u7w,1:u94,1:v5g,1:sm6,1:trr,1:fqm,1:fzb,1:g25,1:gcp,1:gdg,1:ggh,1:gwt,1:gzq,1:h2g,1:hfk,1:hig,1:i90,1:i9a,1:j2s,1:j8o,1:j9q,1:jci,1:k0f,1:k4i,1:k9d,1:kn9,1:lmf,1:lqy,1:lya,1:mem,1:mjv,1:mxu,1:ntd,1:ny6,1:nyh,1:nyg,1:nyo,1:nyu,1:nz1,1:o0d,1:o0e,1:o5c,1:o75,1:og0,1:ovo,1:ow9,1:oxt,1:p6a,1:pad,1:pnx,1:prt,1:prt,1:q53,1:r2o,1:r6e,1:rgh,1:ri1,1:rtf,1:ruw,1:sdy,1:shk,1:tsz,1:u0v,1:u2j,1:j1x,1:36cu,1:pp4,3:ffq,1:g51,1:g2w,1:fpc,1:g1x,1:g3k,1:gcn,1:ge2,1:gwt,1:gut,1:gwp,1:gyq,1:hga,1:hir,1:hms,1:hn8,1:huq,1:i08,1:iqa,1:iqh,1:iuh,1:iwd,1:j4o,1:j7y,1:j60,1:j9q,1:j8g,1:jci,1:jec,1:jpg,1:jrw,1:jte,1:k0m,1:k8k,1:kd3,1:kd7,1:keu,1:l89,1:la2,1:lkx,1:lwb,1:lvv,1:lya,1:m5a,1:mem,1:nmv,1:mk5,1:mnj,1:mqy,1:n01,1:n46,1:n5n,1:nbx,1:nbz,1:ngq,1:nh7,1:nhw,1:nka,1:nk0,1:nv0,1:o8h,1:og0,1:ol7,1:orf,1:ovo,1:p4u,1:pad,1:pxu,1:q27,1:qrd,1:r41,1:r5y,1:r6e,1:ren,1:rg8,1:rez,1:rgh,1:rge,1:rfx,1:ri1,1:rka,1:ruw,1:sag,1:sj6,1:srd,1:sxi,1:tq4,1:tsz,1:tw6,1:tzv,1:u0v,1:u17,1:u2j,1:umq,1:vj0,1:313e,1:3138,1:33dh,1:brx,1:cns,1:cop,1:39e1,1:3bgw,1:3i6r,1:vgj,1:vim,1w:15l.14k,2:16a.14n,b:161.14x,1:161.14y,1:161.14s.14x,1:161.14s.14y,1:15c.14n,1:15c.14o,1:15c.14s,1:15d.14s,1:15e.14s,1:15f.14s,1:15g.14s,1:15h.14s,1:15i.14s,2:15k.14s,1:15l.14s,1:15m.14s,1:15n.14s,1:15o.14s,2:15q.14s,2:15s.14s,1:15t.14s,2:15v.14s,1:15w.14s,2:15y.14s,1:15z.14s,1:160.14s,1:161.14s,1:162.14s,1:15h.14p,1:15d.14v,1:15n.14v,1:15w.14v,47g:1huh.1hve,2:1huj.1hve,f:1hut.1hve,3n:1hyp.1hyf,1:1hyq.1hyf,f0:1idj.1ida,1:1idj.1idz,a7:1int.1inu,1:1int.1ink,2:1int.1inx,70:1iuw.1iun,1:1iux.1iun,ot:1jjp.1jjk,10di:2jx3.2jxh,1:2jx4.2jxh,1:2jx4.2jxh.2jxq,1:2jx4.2jxh.2jxr,1:2jx4.2jxh.2jxs,1:2jx4.2jxh.2jxt,1:2jx4.2jxh.2jxu,2f:2jzt.2jxh,1:2jzu.2jxh,1:2jzt.2jxh.2jxq,1:2jzu.2jxh.2jxq,1:2jzt.2jxh.2jxr,1:2jzu.2jxh.2jxr,1m4g:fgd,1:fg8,1:fgh,1:2tcy,1:fog,1:fqm,1:fqz,1:fsy,1:fwa,1:fx5,1:fzb,1:fyn,1:ae6,1:2ud6,1:g25,1:g2c,1:g2s,1:g3b,1:2u58,1:aex,1:g2v,1:g3x,1:2u6j,1:g47,1:g4k,1:fkc,1:g4s,1:g51,1:3ly7,1:g6t,1:g77,1:afz,1:g8r,1:g92,1:gaa,1:gaf,1:ahh,1:gcn,1:gcp,1:gdg,1:ge2,1:ged,1:gee,1:gev,1:gg9,1:ggh,1:ggq,1:ghf,1:ghp,1:ghr,1:ghr,1:ghr,1:2v58,1:m7k,1:gju,1:gkf,1:2vdv,1:gkr,1:gkx,1:gli,1:gpq,1:gmw,1:gnc,1:go8,1:gpu,1:gs6,1:gsw,1:gur,1:gv7,1:gw4,1:gw4,1:gwp,1:gx7,1:gxf,1:gxu,1:h7a,1:gzq,1:h7b,1:h1t,1:h2s,1:g7b,1:hke,1:hce,1:hdg,1:he5,1:haj,1:hf6,1:hf5,1:hik,1:2x9g,1:hki,1:hkn,1:hl2,1:hlm,1:hlu,1:hnm,1:2xm0,1:2xnu,1:hrg,1:hsr,1:ht3,1:hqw,1:huu,1:aum,1:av0,1:hzc,1:i0u,1:i0u,1:2y88,1:i4j,1:i54,1:i5j,1:i5v,1:2yhk,1:i67,1:i6e,1:itv,1:i76,1:ayp,1:i8w,1:i9a,1:ibk,1:ia5,1:2z1g,1:if7,1:2z1i,1:ige,1:igb,1:igs,1:ijl,1:ijm,1:b3j,1:ikd,1:ilk,1:im5,1:ind,1:b4y,1:2zr7,1:b5o,1:ipc,1:ipf,1:ipi,1:iq2,1:3pg2,1:iri,1:3035,1:3035,1:poh,1:isi,1:isi,1:b7r,1:335k,1:3cgq,1:iua,1:iuj,1:b8j,1:ivu,1:ix9,1:ixj,1:iyh,1:j29,1:bay,1:ba4,1:j2s,1:30t0,1:j47,1:j7s,1:j7w,1:j7y,1:j7w,1:j96,1:j9q,1:jaq,1:jac,1:jan,1:jby,1:jci,1:jcm,1:jdc,1:jdn,1:jfh,1:jht,1:jis,1:jm8,1:31n0,1:jlp,1:jjw,1:jmw,1:jnn,1:jqc,1:31td,1:js2,1:jph,1:jop,1:bhq,1:ju1,1:jum,1:jvh,1:juf,1:bjg,1:k0f,1:k18,1:32mi,1:k4j,1:kc8,1:k7d,1:bo9,1:k9d,1:bns,1:bms,1:g42,1:g45,1:kcg,1:k9o,1:pf1,1:deh,1:kd3,1:kd7,1:kdd,1:kf2,1:ker,1:33cz,1:bpl,1:kje,1:kg5,1:klu,1:kn9,1:33hp,1:kni,1:kkf,1:kr8,1:brx,1:ksi,1:kv7,1:kx6,1:l2g,1:33xf,1:l3v,1:bvc,1:l5t,1:34br,1:l78,1:bwu,1:l82,1:l9b,1:la2,1:la3,1:34p9,1:2yvf,1:34sa,1:le6,1:354s,1:lhb,1:lhp,1:lev,1:ljq,1:lku,1:lmf,1:lkx,1:lm1,1:lmg,1:lmt,1:357i,1:lkk,1:lrj,1:lta,1:c37,1:lvv,1:lvr,1:35jl,1:lq1,1:m0e,1:35ni,1:35ou,1:m2u,1:m61,1:m5a,1:m57,1:c5y,1:m6i,1:m7x,1:m7r,1:m99,1:2u5h,1:mdh,1:368z,1:mfw,1:36i3,1:mjs,1:mk5,1:mkw,1:36yw,1:mm8,1:mmt,1:3779,1:37dg,1:mt6,1:mtn,1:cdo,1:mud,1:ce0,1:ce0,1:myv,1:mzg,1:n01,1:n0l,1:n2i,1:cgr,1:n50,1:386u,1:n5q,1:389e,1:n74,1:2zrz,1:nbk,1:38v5,1:38vs,1:38zo,1:cn0,1:cnc,1:nhw,1:394j,1:394i,1:395l,1:396b,1:nj2,1:nj3,1:nj3,1:nka,1:cop,1:nm3,1:cp2,1:cra,1:39r1,1:nri,1:nt8,1:nv0,1:ctf,1:3a5i,1:nyu,1:3a8q,1:3a9x,1:o0f,1:o2z,1:cvj,1:o5c,1:o5m,1:o5r,1:3at8,1:3b1j,1:3b1j,1:oa6,1:d1e,1:3b8r,1:og6,1:og9,1:d2f,1:3beo,1:onm,1:d5s,1:oo8,1:oo3,1:oow,1:3c06,1:orn,1:d8h,1:ouf,1:ow2,1:oxx,1:d9w,1:3ciw,1:3cjr,1:dax,1:3cnt,1:p6i,1:3cqm,1:p79,1:pa2,1:pad,1:3d22,1:3d43,1:pcw,1:3d7s,1:pdc,1:33a7,1:ded,1:pf6,1:phf,1:dfv,1:pj2,1:hx1,1:3dlz,1:3dmd,1:33bn,1:33bw,1:poh,1:pok,1:sda,1:dij,1:psh,1:psb,1:pst,1:gc3,1:ptd,1:ptf,1:ptp,1:puu,1:3ebg,1:put,1:pwd,1:pyb,1:q0d,1:pwj,1:q0t,1:q1z,1:q53,1:pxv,1:q16,1:q18,1:q1o,1:3eie,1:3eqz,1:3emt,1:dnv,1:q9d,1:q9f,1:qae,1:3g0a,1:qck,1:3f3g,1:dp9,1:dpd,1:3f75,1:3ff6,1:dpn,1:qj4,1:qjg,1:qjr,1:qjt,1:qll,1:qko,1:qoe,1:qn6,1:qrd,1:qp4,1:qqz,1:qrq,1:dsn,1:qu9,1:qv5,1:dtl,1:qxs,1:qxv,1:3giv,1:r13,1:r1a,1:dv9,1:r22,1:aez,1:3gz2,1:3h46,1:dz2,1:dzb,1:rds,1:rfx,1:rka,1:rpx,1:3hrc,1:rsb,1:rsx,1:rvf,1:rxz,1:3i9b,1:2upw,1:s0b,1:rzw,1:s1c,1:2uvy,1:s7o,1:sag,1:3jki,1:3jl9,1:sk4,1:smp,1:snl,1:3ju6,1:snv,1:svs,1:t07,1:t08,1:sxo,1:t89,1:t91,1:3ksa,1:tjf,1:ej9,1:tkn,1:3l2v,1:eli,1:ts3,1:iia,1:tur,1:3ltx,1:3lzu,1:epa,1:epi,1:u00,1:3mdm,1:er6,1:3mhi,1:u17,1:u17,1:u21,1:3mpi,1:u76,1:eur,1:u95,1:ucn,1:ude,1:uf2,1:ez2,1:3nsg,1:umq,1:uv4,1:v0d,1:f66,1:f71,1:v3b,1:3owe,1:f7c,1:3oxx,1:3p5a,1:3p8x,1:vcr,1:f9y,1:veh,1:vem,1:vet,1:vf3,1:vfa,1:vgb,1:3pxc"; export const PRIMARY_COMPOSITES = "5c,1,1,1,1,1,2,1,1,1,1,1,1,1,1,2,1,1,1,1,1,3,1,1,1,1,3,1,1,1,1,1,2,1,1,1,1,1,1,1,1,2,1,1,1,1,1,3,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,4,1,1,1,2,1,1,1,1,1,5,1,1,1,1,1,4,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,y,1,e,1,t,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,4,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,9e,1,2,1,1,2,2,1,1,q,1,1,1,1,1,1,q,1,1,1,1,5,1,18,1,2,4,5,1,1,b,w,n,1,2,4,5,1,1,o,1,22,1,e,1,1,1,3,1,3,1,1,1,1,1,3,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,3,1,89,1,1,1,1,4a,2,h,gm,8,3,47,1,ak,3,1,20,1i,1,1,3g,3c,7,1,2,1,3j,1,1,3y,2,1,1,g8,25c,2,2,2,2,4,15,2,3,1,2,jh,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,3,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,2,5,1,1,1,2,1,1,2,2,1,1,1,1,1,1,4,1,1,1,1,3,1,1,1,1,1,2,1,1,1,1,1,1,2,1,5,1,1,2,1,1,2,2,bi,1,j,v,1,1,1h,5,3,o,2,r,3,3,2,n,2,b,1,1,1,1,3,1,3,1,7,1,3,1,3,1,z,1,1,1,1d,1,1,1,7,1,1,1,2n3,2,2,2,2,2,2,2,2,2,2,2,3,2,2,7,1,2,1,2,1,2,1,2,1,n,a,e,2,2,2,2,2,2,2,2,2,2,2,3,2,2,7,1,2,1,2,1,2,1,2,1,n,3,1,1,1,4,1864,2,f,3n,1,f0,1,a7,1,2,70,1,ot"; export const CASE_FOLDING = "1t:2p,1:2q,1:2r,1:2s,1:2t,1:2u,1:2v,1:2w,1:2x,1:2y,1:2z,1:30,1:31,1:32,1:33,1:34,1:35,1:36,1:37,1:38,1:39,1:3a,1:3b,1:3c,1:3d,1:3e,2j:qk,b:68,1:69,1:6a,1:6b,1:6c,1:6d,1:6e,1:6f,1:6g,1:6h,1:6i,1:6j,1:6k,1:6l,1:6m,1:6n,1:6o,1:6p,1:6q,1:6r,1:6s,1:6t,1:6u,2:6w,1:6x,1:6y,1:6z,1:70,1:71,1:72,1:37.37,x:75,2:77,2:79,2:7b,2:7d,2:7f,2:7h,2:7j,2:7l,2:7n,2:7p,2:7r,2:7t,2:7v,2:7x,2:7z,2:81,2:83,2:85,2:87,2:89,2:8b,2:8d,2:8f,2:2x.lj,2:8j,2:8l,2:8n,3:8q,2:8s,2:8u,2:8w,2:8y,2:90,2:92,2:94,2:jg.32,1:97,2:99,2:9b,2:9d,2:9f,2:9h,2:9j,2:9l,2:9n,2:9p,2:9r,2:9t,2:9v,2:9x,2:9z,2:a1,2:a3,2:a5,2:a7,2:a9,2:ab,2:ad,2:af,2:73,1:ai,2:ak,2:am,2:37,2:gj,1:ar,2:at,2:gk,1:aw,2:gm,1:gn,1:b0,3:d9,1:gp,1:gr,1:b6,2:gw,1:gz,2:h5,1:h4,1:bd,4:hb,1:he,2:hh,1:bl,2:bn,2:bp,2:hs,1:bs,2:hv,3:bx,2:i0,1:c0,2:i2,1:i3,1:c4,2:c6,2:ia,1:c9,4:cd,8:cm,1:cm,2:cp,1:cp,2:cs,1:cs,2:cu,2:cw,2:cy,2:d0,2:d2,2:d4,2:d6,2:d8,3:db,2:dd,2:df,2:dh,2:dj,2:dl,2:dn,2:dp,2:dr,2:2y.lo,1:dv,1:dv,2:dx,2:b9,1:cf,1:e1,2:e3,2:e5,2:e7,2:e9,2:eb,2:ed,2:ef,2:eh,2:ej,2:el,2:en,2:ep,2:er,2:et,2:ev,2:ex,2:ez,2:f1,2:f3,2:bi,2:f7,2:f9,2:fb,2:fd,2:ff,2:fh,2:fj,2:fl,2:fn,8:8rp,1:fw,2:be,1:8rq,3:g2,2:ao,1:i1,1:i4,1:g7,2:g9,2:gb,2:gd,2:gf,6v:qh,17:oh,2:oj,4:on,9:s3,7:q4,2:q5,1:q6,1:q7,2:r0,2:r1,1:r2,1:qh.lk.ld,1:q9,1:qa,1:qb,1:qc,1:qd,1:qe,1:qf,1:qg,1:qh,1:qi,1:qj,1:qk,1:ql,1:qm,1:qn,1:qo,1:qp,2:qr,1:qs,1:qt,1:qu,1:qv,1:qw,1:qx,1:qy,1:qz,5:qt.lk.ld,i:qr,d:rb,1:qa,1:qg,4:qu,1:qo,2:rd,2:rf,2:rh,2:rj,2:rl,2:rn,2:rp,2:rr,2:rt,2:rv,2:rx,2:rz,2:qi,1:qp,3:qg,1:qd,2:s8,2:s2,1:sb,3:or,1:os,1:ot,1:uo,1:up,1:uq,1:ur,1:us,1:ut,1:uu,1:uv,1:uw,1:ux,1:uy,1:uz,1:v0,1:v1,1:v2,1:v3,1:ts,1:tt,1:tu,1:tv,1:tw,1:tx,1:ty,1:tz,1:u0,1:u1,1:u2,1:u3,1:u4,1:u5,1:u6,1:u7,1:u8,1:u9,1:ua,1:ub,1:uc,1:ud,1:ue,1:uf,1:ug,1:uh,1:ui,1:uj,1:uk,1:ul,1:um,1:un,1d:v5,2:v7,2:v9,2:vb,2:vd,2:vf,2:vh,2:vj,2:vl,2:vn,2:vp,2:vr,2:vt,2:vv,2:vx,2:vz,2:w1,a:wb,2:wd,2:wf,2:wh,2:wj,2:wl,2:wn,2:wp,2:wr,2:wt,2:wv,2:wx,2:wz,2:x1,2:x3,2:x5,2:x7,2:x9,2:xb,2:xd,2:xf,2:xh,2:xj,2:xl,2:xn,2:xp,2:xr,2:y7,1:xu,2:xw,2:xy,2:y0,2:y2,2:y4,2:y6,3:y9,2:yb,2:yd,2:yf,2:yh,2:yj,2:yl,2:yn,2:yp,2:yr,2:yt,2:yv,2:yx,2:yz,2:z1,2:z3,2:z5,2:z7,2:z9,2:zb,2:zd,2:zf,2:zh,2:zj,2:zl,2:zn,2:zp,2:zr,2:zt,2:zv,2:zx,2:zz,2:101,2:103,2:105,2:107,2:109,2:10b,2:10d,2:10f,2:10h,2:10j,2:10l,2:10n,2:10p,2:10r,2:10t,2:10v,3:129,1:12a,1:12b,1:12c,1:12d,1:12e,1:12f,1:12g,1:12h,1:12i,1:12j,1:12k,1:12l,1:12m,1:12n,1:12o,1:12p,1:12q,1:12r,1:12s,1:12t,1:12u,1:12v,1:12w,1:12x,1:12y,1:12z,1:130,1:131,1:132,1:133,1:134,1:135,1:136,1:137,1:138,1:139,1:13a,1d:12d.136,26x:8w0,1:8w1,1:8w2,1:8w3,1:8w4,1:8w5,1:8w6,1:8w7,1:8w8,1:8w9,1:8wa,1:8wb,1:8wc,1:8wd,1:8we,1:8wf,1:8wg,1:8wh,1:8wi,1:8wj,1:8wk,1:8wl,1:8wm,1:8wn,1:8wo,1:8wp,1:8wq,1:8wr,1:8ws,1:8wt,1:8wu,1:8wv,1:8ww,1:8wx,1:8wy,1:8wz,1:8x0,1:8x1,2:8x3,6:8x9,mj:3xs,1:3xt,1:3xu,1:3xv,1:3xw,1:3xx,1oj:tu,1:tw,1:u6,1:u9,1:ua,1:ua,1:ui,1:v7,1:wuj,8:3bk,1:3bl,1:3bm,1:3bn,1:3bo,1:3bp,1:3bq,1:3br,1:3bs,1:3bt,1:3bu,1:3bv,1:3bw,1:3bx,1:3by,1:3bz,1:3c0,1:3c1,1:3c2,1:3c3,1:3c4,1:3c5,1:3c6,1:3c7,1:3c8,1:3c9,1:3ca,1:3cb,1:3cc,1:3cd,1:3ce,1:3cf,1:3cg,1:3ch,1:3ci,1:3cj,1:3ck,1:3cl,1:3cm,1:3cn,1:3co,1:3cp,1:3cq,3:3ct,1:3cu,1:3cv,8x:5xd,2:5xf,2:5xh,2:5xj,2:5xl,2:5xn,2:5xp,2:5xr,2:5xt,2:5xv,2:5xx,2:5xz,2:5y1,2:5y3,2:5y5,2:5y7,2:5y9,2:5yb,2:5yd,2:5yf,2:5yh,2:5yj,2:5yl,2:5yn,2:5yp,2:5yr,2:5yt,2:5yv,2:5yx,2:5yz,2:5z1,2:5z3,2:5z5,2:5z7,2:5z9,2:5zb,2:5zd,2:5zf,2:5zh,2:5zj,2:5zl,2:5zn,2:5zp,2:5zr,2:5zt,2:5zv,2:5zx,2:5zz,2:601,2:603,2:605,2:607,2:609,2:60b,2:60d,2:60f,2:60h,2:60j,2:60l,2:60n,2:60p,2:60r,2:60t,2:60v,2:60x,2:60z,2:611,2:613,2:615,2:617,2:619,2:61b,2:61d,2:61f,2:61h,2:2w.mp,1:38.lk,1:3b.lm,1:3d.lm,1:2p.ji,1:601,3:37.37,2:61t,2:61v,2:61x,2:61z,2:621,2:623,2:625,2:627,2:629,2:62b,2:62d,2:62f,2:62h,2:62j,2:62l,2:62n,2:62p,2:62r,2:62t,2:62v,2:62x,2:62z,2:631,2:633,2:635,2:637,2:639,2:63b,2:63d,2:63f,2:63h,2:63j,2:63l,2:63n,2:63p,2:63r,2:63t,2:63v,2:63x,2:63z,2:641,2:643,2:645,2:647,2:649,2:64b,2:64d,2:64f,a:64g,1:64h,1:64i,1:64j,1:64k,1:64l,1:64m,1:64n,9:64w,1:64x,1:64y,1:64z,1:650,1:651,b:65c,1:65d,1:65e,1:65f,1:65g,1:65h,1:65i,1:65j,9:65s,1:65t,1:65u,1:65v,1:65w,1:65x,1:65y,1:65z,9:668,1:669,1:66a,1:66b,1:66c,1:66d,3:qt.lv,2:qt.lv.lc,2:qt.lv.ld,2:qt.lv.n6,3:66p,2:66r,2:66t,2:66v,9:674,1:675,1:676,1:677,1:678,1:679,1:67a,1:67b,h:64g.qh,1:64h.qh,1:64i.qh,1:64j.qh,1:64k.qh,1:64l.qh,1:64m.qh,1:64n.qh,1:64g.qh,1:64h.qh,1:64i.qh,1:64j.qh,1:64k.qh,1:64l.qh,1:64m.qh,1:64n.qh,1:65c.qh,1:65d.qh,1:65e.qh,1:65f.qh,1:65g.qh,1:65h.qh,1:65i.qh,1:65j.qh,1:65c.qh,1:65d.qh,1:65e.qh,1:65f.qh,1:65g.qh,1:65h.qh,1:65i.qh,1:65j.qh,1:674.qh,1:675.qh,1:676.qh,1:677.qh,1:678.qh,1:679.qh,1:67a.qh,1:67b.qh,1:674.qh,1:675.qh,1:676.qh,1:677.qh,1:678.qh,1:679.qh,1:67a.qh,1:67b.qh,3:67k.qh,1:q9.qh,1:q4.qh,2:q9.n6,1:q9.n6.qh,1:69c,1:69d,1:67k,1:67l,1:q9.qh,2:qh,4:67o.qh,1:qf.qh,1:q6.qh,2:qf.n6,1:qf.n6.qh,1:67m,1:67n,1:67o,1:67p,1:qf.qh,6:qh.lk.lc,1:qh.lk.ld,3:qh.n6,1:qh.lk.n6,1:6a8,1:6a9,1:67q,1:67r,7:qt.lk.lc,1:qt.lk.ld,1:qp.lv,2:qt.n6,1:qt.lk.n6,1:6ao,1:6ap,1:67u,1:67v,1:6at,6:67w.qh,1:qx.qh,1:r2.qh,2:qx.n6,1:qx.n6.qh,1:67s,1:67t,1:67w,1:67x,1:qx.qh,8a:qx,4:2z,1:6d,7:6ku,1a:6ls,1:6lt,1:6lu,1:6lv,1:6lw,1:6lx,1:6ly,1:6lz,1:6m0,1:6m1,1:6m2,1:6m3,1:6m4,1:6m5,1:6m6,1:6m7,k:6mc,mr:79s,1:79t,1:79u,1:79v,1:79w,1:79x,1:79y,1:79z,1:7a0,1:7a1,1:7a2,1:7a3,1:7a4,1:7a5,1:7a6,1:7a7,1:7a8,1:7a9,1:7aa,1:7ab,1:7ac,1:7ad,1:7ae,1:7af,1:7ag,1:7ah,1f5:8q8,1:8q9,1:8qa,1:8qb,1:8qc,1:8qd,1:8qe,1:8qf,1:8qg,1:8qh,1:8qi,1:8qj,1:8qk,1:8ql,1:8qm,1:8qn,1:8qo,1:8qp,1:8qq,1:8qr,1:8qs,1:8qt,1:8qu,1:8qv,1:8qw,1:8qx,1:8qy,1:8qz,1:8r0,1:8r1,1:8r2,1:8r3,1:8r4,1:8r5,1:8r6,1:8r7,1:8r8,1:8r9,1:8ra,1:8rb,1:8rc,1:8rd,1:8re,1:8rf,1:8rg,1:8rh,1:8ri,1:8rj,1d:8rl,2:h7,1:5tp,1:hp,3:8rs,2:8ru,2:8rw,2:gh,1:hd,1:gg,1:gi,2:8s3,3:8s6,9:fz,1:g0,1:8sh,2:8sj,2:8sl,2:8sn,2:8sp,2:8sr,2:8st,2:8sv,2:8sx,2:8sz,2:8t1,2:8t3,2:8t5,2:8t7,2:8t9,2:8tb,2:8td,2:8tf,2:8th,2:8tj,2:8tl,2:8tn,2:8tp,2:8tr,2:8tt,2:8tv,2:8tx,2:8tz,2:8u1,2:8u3,2:8u5,2:8u7,2:8u9,2:8ub,2:8ud,2:8uf,2:8uh,2:8uj,2:8ul,2:8un,2:8up,2:8ur,2:8ut,2:8uv,2:8ux,2:8uz,2:8v1,2:8v3,2:8v5,2:8v7,9:8vg,2:8vi,5:8vn,nym:wu9,2:wub,2:wud,2:wuf,2:wuh,2:wuj,2:wul,2:wun,2:wup,2:wur,2:wut,2:wuv,2:wux,2:wuz,2:wv1,2:wv3,2:wv5,2:wv7,2:wv9,2:wvb,2:wvd,2:wvf,2:wvh,k:ww1,2:ww3,2:ww5,2:ww7,2:ww9,2:wwb,2:wwd,2:wwf,2:wwh,2:wwj,2:wwl,2:wwn,2:wwp,2:wwr,3s:x0j,2:x0l,2:x0n,2:x0p,2:x0r,2:x0t,2:x0v,4:x0z,2:x11,2:x13,2:x15,2:x17,2:x19,2:x1b,2:x1d,2:x1f,2:x1h,2:x1j,2:x1l,2:x1n,2:x1p,2:x1r,2:x1t,2:x1v,2:x1x,2:x1z,2:x21,2:x23,2:x25,2:x27,2:x29,2:x2b,2:x2d,2:x2f,2:x2h,2:x2j,2:x2l,2:x2n,b:x2y,2:x30,2:5tl,1:x33,2:x35,2:x37,2:x39,2:x3b,5:x3g,2:h1,3:x3l,2:x3n,4:x3r,2:x3t,2:x3v,2:x3x,2:x3z,2:x41,2:x43,2:x45,2:x47,2:x49,2:h2,1:gs,1:gx,1:h8,1:h6,2:im,1:hz,1:il,1:xub,1:x4l,2:x4n,2:x4p,2:x4r,2:x4t,2:x4v,2:x4x,2:x4z,2:x3o,1:hu,1:5u6,1:x54,2:x56,7:x5d,6:x5j,2:x5l,t:x6e,or:3vk,1:3vl,1:3vm,1:3vn,1:3vo,1:3vp,1:3vq,1:3vr,1:3vs,1:3vt,1:3vu,1:3vv,1:3vw,1:3vx,1:3vy,1:3vz,1:3w0,1:3w1,1:3w2,1:3w3,1:3w4,1:3w5,1:3w6,1:3w7,1:3w8,1:3w9,1:3wa,1:3wb,1:3wc,1:3wd,1:3we,1:3wf,1:3wg,1:3wh,1:3wi,1:3wj,1:3wk,1:3wl,1:3wm,1:3wn,1:3wo,1:3wp,1:3wq,1:3wr,1:3ws,1:3wt,1:3wu,1:3wv,1:3ww,1:3wx,1:3wy,1:3wz,1:3x0,1:3x1,1:3x2,1:3x3,1:3x4,1:3x5,1:3x6,1:3x7,1:3x8,1:3x9,1:3xa,1:3xb,1:3xc,1:3xd,1:3xe,1:3xf,1:3xg,1:3xh,1:3xi,1:3xj,1:3xk,1:3xl,1:3xm,1:3xn,1:3xo,1:3xp,1:3xq,1:3xr,fnl:2u.2u,1:2u.2x,1:2u.30,1:2u.2u.2x,1:2u.2u.30,1:37.38,1:37.38,d:12s.12u,1:12s.12d,1:12s.12j,1:132.12u,1:12s.12l,sq:1ef5,1:1ef6,1:1ef7,1:1ef8,1:1ef9,1:1efa,1:1efb,1:1efc,1:1efd,1:1efe,1:1eff,1:1efg,1:1efh,1:1efi,1:1efj,1:1efk,1:1efl,1:1efm,1:1efn,1:1efo,1:1efp,1:1efq,1:1efr,1:1efs,1:1eft,1:1efu,xy:1fe0,1:1fe1,1:1fe2,1:1fe3,1:1fe4,1:1fe5,1:1fe6,1:1fe7,1:1fe8,1:1fe9,1:1fea,1:1feb,1:1fec,1:1fed,1:1fee,1:1fef,1:1feg,1:1feh,1:1fei,1:1fej,1:1fek,1:1fel,1:1fem,1:1fen,1:1feo,1:1fep,1:1feq,1:1fer,1:1fes,1:1fet,1:1feu,1:1fev,1:1few,1:1fex,1:1fey,1:1fez,1:1ff0,1:1ff1,1:1ff2,1:1ff3,3t:1fiw,1:1fix,1:1fiy,1:1fiz,1:1fj0,1:1fj1,1:1fj2,1:1fj3,1:1fj4,1:1fj5,1:1fj6,1:1fj7,1:1fj8,1:1fj9,1:1fja,1:1fjb,1:1fjc,1:1fjd,1:1fje,1:1fjf,1:1fjg,1:1fjh,1:1fji,1:1fjj,1:1fjk,1:1fjl,1:1fjm,1:1fjn,1:1fjo,1:1fjp,1:1fjq,1:1fjr,1:1fjs,1:1fjt,1:1fju,1:1fjv,4d:1fo7,1:1fo8,1:1fo9,1:1foa,1:1fob,1:1foc,1:1fod,1:1foe,1:1fof,1:1fog,1:1foh,2:1foj,1:1fok,1:1fol,1:1fom,1:1fon,1:1foo,1:1fop,1:1foq,1:1for,1:1fos,1:1fot,1:1fou,1:1fov,1:1fow,1:1fox,2:1foz,1:1fp0,1:1fp1,1:1fp2,1:1fp3,1:1fp4,1:1fp5,2:1fp7,1:1fp8,1d7:1h34,1:1h35,1:1h36,1:1h37,1:1h38,1:1h39,1:1h3a,1:1h3b,1:1h3c,1:1h3d,1:1h3e,1:1h3f,1:1h3g,1:1h3h,1:1h3i,1:1h3j,1:1h3k,1:1h3l,1:1h3m,1:1h3n,1:1h3o,1:1h3p,1:1h3q,1:1h3r,1:1h3s,1:1h3t,1:1h3u,1:1h3v,1:1h3w,1:1h3x,1:1h3y,1:1h3z,1:1h40,1:1h41,1:1h42,1:1h43,1:1h44,1:1h45,1:1h46,1:1h47,1:1h48,1:1h49,1:1h4a,1:1h4b,1:1h4c,1:1h4d,1:1h4e,1:1h4f,1:1h4g,1:1h4h,1:1h4i,2cu:1jgg,1:1jgh,1:1jgi,1:1jgj,1:1jgk,1:1jgl,1:1jgm,1:1jgn,1:1jgo,1:1jgp,1:1jgq,1:1jgr,1:1jgs,1:1jgt,1:1jgu,1:1jgv,1:1jgw,1:1jgx,1:1jgy,1:1jgz,1:1jh0,1:1jh1,1:1jh2,1:1jh3,1:1jh4,1:1jh5,1:1jh6,1:1jh7,1:1jh8,1:1jh9,1:1jha,1:1jhb,gw1:20dc,1:20dd,1:20de,1:20df,1:20dg,1:20dh,1:20di,1:20dj,1:20dk,1:20dl,1:20dm,1:20dn,1:20do,1:20dp,1:20dq,1:20dr,1:20ds,1:20dt,1:20du,1:20dv,1:20dw,1:20dx,1:20dy,1:20dz,1:20e0,1:20e1,1:20e2,1:20e3,1:20e4,1:20e5,1:20e6,1:20e7,o81:2oma,1:2omb,1:2omc,1:2omd,1:2ome,1:2omf,1:2omg,1:2omh,1:2omi,1:2omj,1:2omk,1:2oml,1:2omm,1:2omn,1:2omo,1:2omp,1:2omq,1:2omr,1:2oms,1:2omt,1:2omu,1:2omv,1:2omw,1:2omx,1:2omy,1:2omz,1:2on0,1:2on1,1:2on2,1:2on3,1:2on4,1:2on5,1:2on6,1:2on7"; +export const STREAM_SAFE = "4o:0.1,7:0.1,5:0.1,4:0.1,8:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,4:0.1,1:0.1,1:0.1,1:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,5:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,4:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,y:0.1,1:0.1,e:0.1,1:0.1,k:0.1,1:0.1,1:0.1,7:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,2:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,4:0.1,1:0.1,3:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,7:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,4l:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,z:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:2.2,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,b:0.1,a:0.1,1:0.2,1:0.1,2:0.1,1:0.1,1:0.1,2:0.1,2:0.1,1:0.1,1:0.2,q:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,q:0.1,1:0.1,1:0.1,1:0.1,1:0.1,5:0.1,1:0.1,18:0.1,1:0.1,2:0.1,4:0.1,5:0.1,1:0.1,1:0.1,b:0.1,w:0.1,n:0.1,1:0.1,2:0.1,4:0.1,5:0.1,1:0.1,1:0.1,o:0.1,1:0.1,c:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1m:0.1,1:0.1,e:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,48:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,2:1.1,2:1.1,1:1.1,2:1.1,1:1.1,2:1.1,21:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,8:0.1,1:0.1,1:0.1,1:0.1,1:0.1,11:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,h:1.1,28:0.1,2:0.1,h:0.1,3:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,3:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,3:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,10:1.1,v:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,4h:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,a:1.1,p:1.1,1:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,1:1.1,18:1.1,1:1.1,1:1.1,1p:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,17:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,16:0.1,8:0.1,3:0.1,8:1.1,h:1.1,4:1.1,1:1.1,1:1.1,1:1.1,4:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2l:1.1,2:1.1,d:0.1,1:0.1,1:1.1,a:1.1,5:0.1,1:0.1,2:0.1,v:1.1,1h:0.1,3:0.1,6:1.1,h:1.1,c:0.1,1:0.1,1:0.1,3:0.1,2m:1.1,h:1.1,33:1.1,2:1.1,a:0.1,3:0.1,1:0.1,1:1.1,9:1.1,1:1.1,5:0.1,1:0.1,1j:0.1,16:1.1,c:0.1,1:0.1,1:0.1,1:1.1,a:1.1,2t:1.1,c:0.1,5:1.1,8:1.1,1:1.1,2u:1.1,4:0.1,2:1.1,5:0.1,1:0.1,2:0.1,1:0.2,2:1.1,8:1.1,1:1.1,2t:1.1,1:1.1,2:1.1,c:0.1,1:0.1,1:0.1,1:1.1,a:1.1,37:1.1,5:1.1,b:0.1,2:0.1,1:0.2,1:0.1,1:1.1,2h:1.1,1:1.1,1:1.1,e:1.1,1:1.1,1:1.1,1:1.1,31:1.1,1:1.1,1:1.1,e:1.1,1:1.1,1:1.1,1:1.1,25:1.1,1:1.1,s:1.1,2:1.1,2:1.1,1k:1.1,1:1.1,1:2.2,1:1.1,1:2.2,1:0.1,1:0.2,1:0.1,1:0.2,1:1.1,1:1.1,1:1.1,1:1.1,3:1.1,1:2.2,1:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1r:1.1,2o:0.1,8:1.1,9:1.1,2:1.1,1:1.1,2b:1.1,5w:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1f:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,bf:1.1,1:1.1,1:1.1,qd:1.1,1:1.1,v:1.1,4e:1.1,b:1.1,5o:1.1,40:1.1,1:1.1,1:1.1,64:1.1,1:1.1,20:1.1,l:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,3:1.1,1d:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1k:0.1,2:0.1,2:0.1,2:0.1,2:0.1,4:0.1,y:1.1,1:1.1,6:0.1,2:0.1,3:0.1,1:0.1,2:0.1,1:1.1,13:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1j:1.1,1:1.1,1n:1.1,c:1.1,1:1.1,1w:1.1,49:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,5:1.1,7:1.1,4:1.1,1:1.1,5j:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,5:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,7:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,3:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,3:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,3:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,3:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,2:0.1,2:0.2,2:0.2,2:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.2,1:0.2,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.2,1:0.2,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.2,1:0.2,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.2,1:0.2,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.2,1:0.2,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.2,1:0.2,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.3,1:0.1,1:0.1,1:0.2,1:0.1,1:0.2,2:0.1,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.2,2:0.1,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,3:0.1,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,2:0.2,1:0.2,1:0.2,1:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.2,1:0.2,4:0.2,1:0.1,1:0.2,2:0.1,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,p:0.1,13:0.1,42:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,5:1.1,4:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1n:0.1,33:0.1,1:0.1,j:0.1,v:0.1,1:0.1,1:0.1,1h:0.1,5:0.1,3:0.1,o:0.1,2:0.1,r:0.1,3:0.1,3:0.1,2:0.1,n:0.1,2:0.1,b:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3:0.1,1:0.1,3:0.1,1:0.1,7:0.1,1:0.1,3:0.1,1:0.1,3:0.1,1:0.1,z:0.1,1:0.1,1:0.1,1:0.1,1d:0.1,1:0.1,1:0.1,1:0.1,7:0.1,1:0.1,1:0.1,1:0.1,1kf:0.1,er:1.1,1:1.1,1:1.1,3y:1.1,2p:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,ff:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,t:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,3:0.1,2:0.1,2:0.1,7:0.1,1:0.1,2:0.1,1:0.1,2:0.1,1:0.1,2:0.1,1:0.1,2:0.1,1:0.1,n:0.1,5:1.1,1:1.1,1:0.1,1:0.1,2:0.1,e:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,2:0.1,3:0.1,2:0.1,2:0.1,7:0.1,1:0.1,2:0.1,1:0.1,2:0.1,1:0.1,2:0.1,1:0.1,2:0.1,1:0.1,n:0.1,3:0.1,1:0.1,1:0.1,1:0.1,4:0.1,1h:1.1,2:1.1,1:1.1,4:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,g:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,7f:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,3q:0.1,3:0.1,9:0.1,e:0.1,3:0.1,h:0.1,9:0.1,5:0.1,b:0.1,3:0.1,mqp:1.1,5:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,x:1.1,1:1.1,29:1.1,1:1.1,7p:1.1,12:1.1,48:1.1,s:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1m:1.1,1:1.1,1:1.1,12:1.1,2o:1.1,d:1.1,6o:1.1,2:1.1,1:1.1,1:1.1,3:1.1,1:1.1,6:1.1,1:1.1,2:1.1,1h:1.1,6v:1.1,j:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,70a:0.1,1:1.1,1:0.1,b:0.1,1:0.1,1:0.2,1:0.2,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,2:0.1,1:0.1,2:0.1,1:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2e:0.1,1:0.1,b:0.1,1:0.1,4q:0.1,1:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,19:0.1,21:0.1,p:0.2,1:0.2,1:0.2,20:0.1,1:0.1,6b:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,q:0.1,1:0.1,1:0.1,1:0.1,10:0.1,1:0.1,1:0.1,2:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,2x:0.1,1:0.1,1:0.1,1:0.1,1:0.1,1:0.1,4k:1.1,1:1.1,4:1.1,2:1.1,1:1.1,4:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,j:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,3:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,3:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,3:1.1,1:1.1,1:1.1,7:0.1,ey:1.1,6b:1.1,46:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1ar:1.1,2:1.1,15:1.1,1:1.1,1:1.1,5:1.1,4m:1.1,1:1.1,fy:1.1,1:1.1,1:1.1,1:1.1,as:1.1,1:1.1,29:1.1,1:1.1,1:1.1,1z:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1e:1.1,1:1.1,1:1.1,1:1.1,5d:1.1,16:1.1,f:1.1,r:0.1,2:0.1,f:0.1,e:1.1,1:1.1,1y:1.1,1:1.1,1:1.1,11:1.1,7:0.1,1:0.1,4:1.1,1:1.1,1r:1.1,25:1.1,a:1.1,2z:1.1,1:1.1,4z:1.1,1:1.1,29:1.1,1:1.1,2:1.1,d:0.1,1:0.1,1:1.1,a:1.1,f:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,4:1.1,1:1.1,1:1.1,1:1.1,1:1.1,5q:1.1,4:1.1,o:1.1,2a:1.1,a:1.1,1:0.1,1:0.1,1:1.1,1:0.1,4:1.1,1:1.1,6k:1.1,b:0.1,1:0.1,4:1.1,1:1.1,3j:1.1,3b:1.1,1:1.1,38:1.1,7i:1.1,1:1.1,6u:1.1,8:0.1,5:1.1,1:1.1,5:1.1,4d:1.1,2c:1.1,j:1.1,2a:1.1,bq:1.1,77:1.1,2:1.1,1:1.1,2a:1.1,bu:1.1,1:1.1,ey6:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1o:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,xm:1.1,1:1.1,f59:1.1,43k:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,1:0.2,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,4:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,9:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,3:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,v:1.1,1:1.1,1:1.1,1:1.1,e:0.1,1:0.1,1:0.2,1:0.2,1:0.2,1:0.2,3m:1.1,1:1.1,1:1.1,2po:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,3:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,2:1.1,1:1.1,2:1.1,1:1.1,1:1.1,1:1.1,1:1.1,2t:1.1,4h:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,ag:1.1,1q:1.1,1:1.1,1:1.1,1:1.1,e5:1.1,1:1.1,1:1.1,1:1.1,rl:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,32:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1:1.1,1qh:0.1"; diff --git a/packages/skill-tree/src/unicode-15.ts b/packages/skill-tree/src/unicode-15.ts index 9b3aee5..b20c8dc 100644 --- a/packages/skill-tree/src/unicode-15.ts +++ b/packages/skill-tree/src/unicode-15.ts @@ -13,6 +13,7 @@ import { COMBINING_CLASSES, FORMAT_RANGES, PRIMARY_COMPOSITES, + STREAM_SAFE, UNICODE_VERSION, } from "./unicode-15-tables"; @@ -62,6 +63,8 @@ const assigned = decodeRanges(ASSIGNED_RANGES); const format = decodeRanges(FORMAT_RANGES); const decompositions = decodeMap(CANONICAL_DECOMPOSITIONS); const folding = decodeMap(CASE_FOLDING); +/** Leading and trailing non-starter counts of x/text norm's Stream-Safe Text Processing. */ +const streamSafe = decodeMap(STREAM_SAFE); const combiningClasses = new Map(); { @@ -129,50 +132,115 @@ function decomposeInto(cp: number, out: number[]): void { else for (const m of mapping) decomposeInto(m, out); } -/** Canonical decomposition with canonical ordering (NFD) over code points. */ -function nfdCodePoints(cps: Iterable): number[] { - const out: number[] = []; - for (const cp of cps) decomposeInto(cp, out); - // Canonical ordering: stable insertion sort of each run of non-starters. - for (let i = 1; i < out.length; i++) { - const cc = combiningClass(out[i]!); - if (cc === 0) continue; - let j = i; - while (j > 0 && combiningClass(out[j - 1]!) > cc) { - [out[j - 1], out[j]] = [out[j]!, out[j - 1]!]; - j--; +/** U+034F COMBINING GRAPHEME JOINER, a starter that never composes. */ +const CGJ = 0x034f; +const MAX_NON_STARTERS = 30; + +/** Code points that are the second of a primary composite pair (x/text "combines backward"). */ +const combinesBackward = new Set(); +for (const key of compositions.keys()) combinesBackward.add(key % 0x110000); + +/** + * Splits code points into the segments Go's x/text norm normalizes one at a + * time, each decomposed and canonically ordered. A segment starts at a code + * point with no leading non-starter. Stream-Safe Text Processing: when a + * code point would extend a run past 30 non-starters (counted over + * compatibility decompositions, as x/text does), a CGJ ends the segment and + * the code point starts the next one, so a string with such a run is never + * NFC. + */ +function segments(cps: Iterable): number[][] { + const out: number[][] = []; + let segment: number[] = []; + let run = 0; + for (const cp of cps) { + const [lead = 0, trail = 0] = streamSafe.get(cp) ?? []; + if (lead > 0 && run + lead > MAX_NON_STARTERS) { + segment.push(CGJ); + run = 0; + out.push(segment); + segment = []; + } else if (lead === 0 && segment.length > 0) { + out.push(segment); + segment = []; + } + run = lead === 0 ? trail : run + lead; + decomposeInto(cp, segment); + } + if (segment.length > 0) out.push(segment); + for (const s of out) { + // Canonical ordering: stable insertion sort of each run of non-starters. + for (let i = 1; i < s.length; i++) { + const cc = combiningClass(s[i]!); + if (cc === 0) continue; + let j = i; + while (j > 0 && combiningClass(s[j - 1]!) > cc) { + [s[j - 1], s[j]] = [s[j]!, s[j - 1]!]; + j--; + } } } return out; } -function composePair(a: number, b: number): number | undefined { - const l = a - L_BASE, v = b - V_BASE; - if (l >= 0 && l < L_COUNT && v >= 0 && v < V_COUNT) return S_BASE + (l * V_COUNT + v) * T_COUNT; - const s = a - S_BASE, t = b - T_BASE; - if (s >= 0 && s < S_COUNT && s % T_COUNT === 0 && t > 0 && t < T_COUNT) return a + t; - return compositions.get(pairKey(a, b)); +/** + * Recomposes one segment exactly as x/text reorderBuffer.compose does. It + * differs from UAX #15 in one way: a starter only becomes the composition + * target when a code point that combines backward follows it directly, so a + * starter such as U+FF9E inside a segment does not block. + */ +function composeSegment(b: number[]): number[] { + const ccc = b.map(combiningClass); + let k = 1; + let s = 0; + const keep = (i: number) => { + b[k] = b[i]!; + ccc[k++] = ccc[i]!; + }; + const assign = (cp: number) => { + b[s] = cp; + ccc[s] = 0; + }; + for (let i = 1; i < b.length; i++) { + if (b[i]! >= 0x1100 && b[i]! <= 0x11ff) { + // Hangul mode for the rest of the segment, as in x/text combineHangul. + for (; i < b.length; i++) { + const cccB = ccc[k - 1]!; + if (cccB === 0) s = k - 1; + if (s !== k - 1 && cccB >= ccc[i]!) { + keep(i); + continue; + } + const l = b[s]! - L_BASE, v = b[i]! - V_BASE, lv = b[s]! - S_BASE, t = b[i]! - T_BASE; + if (l >= 0 && l < L_COUNT && v >= 0 && v < V_COUNT) assign(S_BASE + (l * V_COUNT + v) * T_COUNT); + else if (lv >= 0 && lv < S_COUNT && lv % T_COUNT === 0 && t > 0 && t < T_COUNT) assign(b[s]! + t); + else keep(i); + } + break; + } + if (combinesBackward.has(b[i]!)) { + const cccB = ccc[k - 1]!; + let blocked = false; + if (cccB === 0) s = k - 1; + else blocked = s !== k - 1 && cccB >= ccc[i]!; + const composite = blocked ? undefined : compositions.get(pairKey(b[s]!, b[i]!)); + if (composite !== undefined) { + assign(composite); + continue; + } + } + keep(i); + } + b.length = Math.min(k, b.length); + return b; +} + +function nfdCodePoints(cps: Iterable): number[] { + return segments(cps).flat(); } function nfcCodePoints(cps: Iterable): number[] { - const d = nfdCodePoints(cps); - if (d.length === 0) return d; - const out = [d[0]!]; - let starterPos = 0; - let lastClass = combiningClass(d[0]!) === 0 ? 0 : 256; - for (let i = 1; i < d.length; i++) { - const ch = d[i]!; - const chClass = combiningClass(ch); - const composite = composePair(out[starterPos]!, ch); - if (composite !== undefined && (lastClass < chClass || lastClass === 0)) { - out[starterPos] = composite; - continue; - } - if (chClass === 0) starterPos = out.length; - lastClass = chClass; - out.push(ch); - } - return out; + return segments(cps).flatMap(composeSegment); } const codePoints = (s: string): number[] => Array.from(s, (ch) => ch.codePointAt(0)!); diff --git a/packages/skill-tree/src/zip-reader.ts b/packages/skill-tree/src/zip-reader.ts index d360735..0aaafd5 100644 --- a/packages/skill-tree/src/zip-reader.ts +++ b/packages/skill-tree/src/zip-reader.ts @@ -57,7 +57,7 @@ async function* rawData(src: RandomAccessSource, start: number, length: number): } /** Content of an entry, inflated when deflated, with its CRC-32 and size checked at the end. */ -async function* content(src: RandomAccessSource, central: CentralEntry, dataStart: number, verify: boolean): AsyncGenerator { +async function* content(src: RandomAccessSource, central: CentralEntry, dataStart: number): AsyncGenerator { const raw = rawData(src, dataStart, central.csize); const data = central.method === 8 ? decompress(raw, "deflate-raw") : raw; let crc = 0; @@ -67,7 +67,7 @@ async function* content(src: RandomAccessSource, central: CentralEntry, dataStar size += chunk.length; yield chunk; } - if (verify && (crc !== central.crc || size !== central.usize)) throw readFailed("zip entry data does not match its CRC-32 or size"); + if (crc !== central.crc || size !== central.usize) throw readFailed("zip entry data does not match its CRC-32 or size"); } async function readEntries(src: RandomAccessSource, collector: EntryCollector, limits: TreeLimits): Promise { @@ -106,16 +106,18 @@ async function readEntries(src: RandomAccessSource, collector: EntryCollector, l fault ??= sizesDisagree(local, c, descriptor); const type = (c.externalAttrs >>> 16) & S_IFMT; const base = { path: name.path, utf8Valid: name.utf8Valid, ...(fault === undefined ? {} : { reason: fault }) }; - if (type === S_IFDIR && fault === undefined) { - collector.add({ ...base, mode: "", size: 0, sha256: "", kind: "directory" }); + if (fault !== undefined) { + // A faulted entry (header disagreement, encryption, unknown method, + // non-regular type) is unsupported: its data is never inflated and it + // counts toward no file or byte limit, as a tar special entry. + collector.add({ ...base, mode: headerMode(c.externalAttrs >>> 16), size: 0, sha256: "", kind: "special" }); continue; } - if (dataEnd > dir.cdOffset) { - collector.add({ ...base, mode: "", size: 0, sha256: "", kind: "special" }); + if (type === S_IFDIR) { + collector.add({ ...base, mode: "", size: 0, sha256: "", kind: "directory" }); continue; } - // Header agreement comes first; only agreeing entries are checked against their CRC-32. - const hashed = await collector.hashFile(name.path, content(src, c, local.dataStart, fault === undefined)); + const hashed = await collector.hashFile(name.path, content(src, c, local.dataStart)); collector.add({ ...base, mode: headerMode(c.externalAttrs >>> 16), ...hashed, kind: "regular" }); } if (pos !== dir.cdOffset) archiveFault ??= "bytes between the last local entry and the central directory"; diff --git a/packages/skill-tree/test/path-fuzz.test.ts b/packages/skill-tree/test/path-fuzz.test.ts index a937304..c2f4f7d 100644 --- a/packages/skill-tree/test/path-fuzz.test.ts +++ b/packages/skill-tree/test/path-fuzz.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { foldKey } from "../src/collision"; import { decodeName } from "../src/entry-collector"; import { buildTree, frontmatterExposure, validatePath } from "../src/index"; import { errorCodeOf } from "./vector-helpers"; @@ -44,6 +45,19 @@ describe("path validation never throws", () => { }); }); +describe("collision keys", () => { + it("fold each segment independently, so directory keys are prefixes of file keys", () => { + const next = rng(0xf01d); + const marks = ["́", "̅", "̖", "̈́", "͏"]; + const tokens = [...ALPHABET, ...marks, ...marks, ...marks]; + for (let i = 0; i < 5_000; i++) { + const path = Array.from({ length: 1 + Math.floor(next() * 40) }, () => tokens[Math.floor(next() * tokens.length)]).join(""); + expect(foldKey(path).split("/"), JSON.stringify(path)).toEqual(path.split("/").map(foldKey)); + } + expect(foldKey("a" + "̅".repeat(20) + "/" + "̅".repeat(20))).toBe(foldKey("a" + "̅".repeat(20)) + "/" + foldKey("̅".repeat(20))); + }); +}); + describe("frontmatter exposure never throws", () => { it.each([ ["", "AUTO", false], diff --git a/packages/skill-tree/test/provenance.test.ts b/packages/skill-tree/test/provenance.test.ts index c38bb59..5ea2b7f 100644 --- a/packages/skill-tree/test/provenance.test.ts +++ b/packages/skill-tree/test/provenance.test.ts @@ -6,7 +6,8 @@ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; -import { UNICODE_VERSION, caseFoldEntries, combiningClass, isAssigned, isFormat, nfc, nfd } from "../src/unicode-15"; +import { validatePath } from "../src/path-rules"; +import { UNICODE_VERSION, caseFoldEntries, combiningClass, isAssigned, isFormat, isNfc, nfc, nfd } from "../src/unicode-15"; import { fixtureBytes, sha256 } from "./vector-helpers"; const PINS: Record = { @@ -14,7 +15,7 @@ const PINS: Record = { "vectors/identity-v1-vectors.json": "1ab9e013c229d78871abd60e3cd3bf26fcfd0e6067d98dcdd79b4f6bcc1daefb", "ucd/CaseFolding-15.0.0.txt": "cdd49e55eae3bbf1f0a3f6580c974a0263cb86a6a08daa10fbf705b4808a56f7", }; -const GO_TABLES_SHA256 = "baa335155dcbaf0cfe8f40fcc0a98232c4134e821a08c3f198f829a51d911ad5"; +const GO_TABLES_SHA256 = "0d0155948ceea434fa241b59937db1a41689fa2ccb8231d35d96a2c56f57b52b"; const MAX_CP = 0x10ffff; interface GoTables { @@ -28,6 +29,13 @@ interface GoTables { const goBytes = readFileSync(fileURLToPath(new URL("../scripts/unicode-15.0.0-go.json", import.meta.url))); const go = JSON.parse(goBytes.toString("utf8")) as GoTables; +const STREAM_SAFE_VECTORS_SHA256 = "de0331152c6f83ed05e2c5013c9d383426731d2c7423c1127f15c86f395ed378"; +const streamSafeBytes = readFileSync(fileURLToPath(new URL("../scripts/stream-safe-15.0.0-go-vectors.json", import.meta.url))); + +interface StreamSafeVectors { + alphabet: number[]; + cases: Array<{ in: string; isNfc: boolean; nfd: string; nfc: string }>; +} function rangeSet(ranges: Array<[number, number]>): Uint8Array { const set = new Uint8Array(MAX_CP + 1); @@ -90,6 +98,41 @@ describe("normalization agrees with the runtime on Unicode 15.0.0 characters", ( expect(checked).toBeGreaterThan(149_000); }); + it("inserts a CGJ into runs of more than 30 non-starters, like Go x/text norm", () => { + const mark = "̅"; + const cases: Array<[string, boolean]> = [ + ["a" + mark.repeat(30), true], + ["a" + mark.repeat(31), false], + [mark.repeat(30), true], + [mark.repeat(31), false], + ["á" + mark.repeat(29), true], // á carries one trailing non-starter + ["á" + mark.repeat(30), false], + ["a" + mark.repeat(29) + "̈́", false], // U+0344 adds two + ["a" + mark.repeat(30) + "͏" + mark.repeat(30), true], + ]; + for (const [s, want] of cases) expect(isNfc(s), `${s.length} code units`).toBe(want); + // Go: the CGJ goes before the overflowing mark, and the next run counts it. + expect(nfd("a" + "́".repeat(62))).toBe( + "a" + "́".repeat(30) + "͏" + "́".repeat(30) + "͏" + "́".repeat(2), + ); + expect(validatePath("x/a" + mark.repeat(31) + ".md")?.code).toBe("tree.non_nfc_path"); + expect(validatePath("x/a" + mark.repeat(30) + ".md")).toBeNull(); + }); + + it("matches the Go stream-safe vectors", () => { + expect(sha256(streamSafeBytes)).toBe(STREAM_SAFE_VECTORS_SHA256); + const vectors = JSON.parse(streamSafeBytes.toString("utf8")) as StreamSafeVectors; + expect(vectors.cases.length).toBe(800); + const short = (s: string) => sha256(s).slice(0, 16); + let normal = 0; + for (const v of vectors.cases) { + const s = String.fromCodePoint(...Array.from(v.in, (c) => vectors.alphabet[c.charCodeAt(0) - 65]!)); + expect([isNfc(s), short(nfd(s)), short(nfc(s))], v.in).toEqual([v.isNfc, v.nfd, v.nfc]); + if (v.isNfc) normal++; + } + expect(normal).toBeGreaterThan(100); + }); + it("composes, reorders and blocks sequences", () => { const samples = [ "é", "Ǻ", "ḍ̇", "q̣̇", "각", "각", diff --git a/packages/skill-tree/test/tar-reader.test.ts b/packages/skill-tree/test/tar-reader.test.ts index ae2826b..1f9b0a7 100644 --- a/packages/skill-tree/test/tar-reader.test.ts +++ b/packages/skill-tree/test/tar-reader.test.ts @@ -2,7 +2,8 @@ import { once } from "node:events"; import { createGzip, gzipSync } from "node:zlib"; import { describe, expect, it } from "vitest"; import { buildTree, readTarTree, readZipTree } from "../src/index"; -import { tarBytes, tarStream } from "./tar-writer"; +import { parsePax } from "../src/tar-header"; +import { concat, paxData, tarBytes, tarHeader, tarStream } from "./tar-writer"; import type { TarSpec } from "./tar-writer"; import { zipBytes } from "./zip-writer"; import { errorCodeOf, sha256 } from "./vector-helpers"; @@ -16,6 +17,35 @@ function file(name: string, data: Uint8Array | string = "x\n", mode = 0o644): Ta const skill = file("SKILL.md", SKILL); +const SPARSE_1_0 = { "GNU.sparse.major": "1", "GNU.sparse.minor": "0" }; + +/** A GNU 1.0 sparse data section: the map (decimal lines, padded to a block), then the data. */ +function sparse1x0Data(data: string, map = "0\n"): Uint8Array { + const block = new Uint8Array(Math.ceil(map.length / 512) * 512); + block.set(enc.encode(map)); + return concat([block, enc.encode(data)]); +} + +/** Overwrites header fields of a tar block and recomputes its checksum. */ +function patchHeader(block: Uint8Array, fields: Array<[number, string | number[]]>): Uint8Array { + const b = block.slice(); + for (const [at, value] of fields) b.set(typeof value === "string" ? enc.encode(value) : value, at); + b.fill(0x20, 148, 156); + const sum = b.reduce((n, x) => n + x, 0); + b.set(enc.encode(`${sum.toString(8).padStart(6, "0")}\0 `), 148); + return b; +} + +const pad = (size: number) => new Uint8Array((512 - (size % 512)) % 512); + +/** One entry as raw blocks: a (patched) header, its data and padding. */ +function rawEntry(name: string, typeflag: string, data: Uint8Array, fields: Array<[number, string | number[]]> = [], size = data.length): Uint8Array { + return concat([patchHeader(tarHeader(enc.encode(name), typeflag, 0o644, size), fields), data, pad(data.length)]); +} + +const END = new Uint8Array(1024); +const skillBytes = () => tarBytes([skill], false); + /** Gzips a stream without holding its uncompressed bytes. */ async function gzipStream(source: AsyncIterable): Promise { const parts: Buffer[] = []; @@ -50,7 +80,7 @@ describe("tar reader rejects unsafe entries", () => { ["hard link", [{ name: enc.encode("hard"), typeflag: "1", mode: 0o644 }], "tree.unsupported_entry"], ["FIFO", [{ name: enc.encode("pipe"), typeflag: "6", mode: 0o644 }], "tree.unsupported_entry"], ["contiguous file", [{ ...file("big.bin"), typeflag: "7" }], "tree.unsupported_entry"], - ["sparse PAX file", [{ ...file("sparse.bin"), pax: { "GNU.sparse.major": "1", "GNU.sparse.minor": "0" } }], "tree.unsupported_entry"], + ["sparse PAX file", [{ ...file("sparse.bin", sparse1x0Data("x")), pax: SPARSE_1_0 }], "tree.unsupported_entry"], ["setuid file", [file("run.sh", "#!/bin/sh\n", 0o4755)], "tree.unsupported_entry"], ["group-writable file", [file("w.md", "x\n", 0o664)], "tree.unsupported_entry"], ] as const)("%s", async (_name, specs, code) => { @@ -151,3 +181,184 @@ describe("one tree, one digest", () => { expect(digests[0]).toMatch(/^sha256:[0-9a-f]{64}$/); }); }); + +describe("tar headers parse like Go archive/tar", () => { + const data = enc.encode("x\n"); + const expected = async () => (await readTarTree(await tarBytes([skill, file("refs/a.md")]))).digest; + const read = async (...parts: Uint8Array[]) => readTarTree(concat([await skillBytes(), ...parts, END])); + const code = (...parts: Uint8Array[]) => errorCodeOf(() => read(...parts)); + const ff = (n: number) => new Array(n).fill(0xff); + + it("cuts an octal field at its first NUL", async () => { + expect((await read(rawEntry("refs/a.md", "0", data, [[100, "644\0zzz\0"]]))).digest).toBe(await expected()); + }); + + it("accepts negative and wide base-256 ids and times", async () => { + const wide = [0x80, 0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff]; + expect((await read(rawEntry("refs/a.md", "0", data, [[108, ff(8)], [116, wide], [136, ff(12)]]))).digest).toBe(await expected()); + }); + + it("rejects numeric fields that do not parse, used or not", async () => { + expect(await code(rawEntry("refs/a.md", "0", data, [[108, "0000009\0"]]))).toBe("tree.read_failed"); + expect(await code(rawEntry("refs/a.md", "0", data, [[329, "zz\0"]]))).toBe("tree.read_failed"); + // A base-256 value beyond int64 overflows, as in Go. + expect(await code(rawEntry("refs/a.md", "0", data, [[136, [0x80, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]]]))).toBe("tree.read_failed"); + }); + + it("rejects a negative size, on files and extension headers", async () => { + expect(await code(rawEntry("refs/a.md", "0", data, [[124, ff(12)]]))).toBe("tree.read_failed"); + expect(await code(rawEntry("PaxHeader", "x", new Uint8Array(0), [[124, ff(12)]]), rawEntry("refs/a.md", "0", data))).toBe("tree.read_failed"); + // Header-only types ignore their size. + expect(await code(rawEntry("refs/", "5", new Uint8Array(0), [[124, ff(12)]]), rawEntry("refs/a.md", "0", data))).toBeNull(); + }); + + it("never truncates a base-256 mode to 32 bits", async () => { + const mode = [0x80, 0, 0, 0x01, 0, 0, 0x01, 0xa4]; // 2^32 + 0o644 + expect(await code(rawEntry("refs/a.md", "0", data, [[100, mode]]))).toBe("tree.unsupported_entry"); + expect(await code(rawEntry("refs/a.md", "0", data, [[100, ff(8)]]))).toBe("tree.unsupported_entry"); + }); + + it("joins a USTAR prefix whatever the version field holds", async () => { + expect((await read(rawEntry("a.md", "0", data, [[263, [0, 0]], [345, "refs\0"]]))).digest).toBe(await expected()); + }); + + it("falls back to a USTAR prefix where a GNU header's times do not parse", async () => { + expect((await read(rawEntry("a.md", "0", data, [[257, "ustar \0"], [345, "refs\0"]]))).digest).toBe(await expected()); + }); + + it("accepts PAX record lengths with a sign or leading zeros", async () => { + for (const len of ["0020", "+020"]) { + const pax = rawEntry("PaxHeader", "x", enc.encode(`${len} path=refs/a.md\n`)); + expect((await read(pax, rawEntry("placeholder.md", "0", data))).digest, len).toBe(await expected()); + } + expect(await code(rawEntry("PaxHeader", "x", enc.encode("-20 path=refs/a.md\n")), rawEntry("a.md", "0", data))).toBe("tree.read_failed"); + }); + + it.each([ + [{ mtime: "1." }, null], + [{ mtime: "-1.5" }, null], + [{ mtime: ".5" }, "tree.read_failed"], + [{ mtime: "1.5x" }, "tree.read_failed"], + [{ uid: "-5" }, null], + [{ uid: "+5" }, null], + [{ uid: "9223372036854775808" }, "tree.read_failed"], + [{ size: "" }, null], + [{ size: "+2" }, null], + ] as const)("PAX values %j", async (pax, want) => { + expect(await tarCode([skill, { ...file("refs/a.md"), pax }])).toBe(want); + }); + + it("an empty GNU long name clears an earlier one", async () => { + const longName = (name: string) => rawEntry("././@LongLink", "L", enc.encode(`${name}\0`)); + expect((await read(longName("refs/long-name.md"), longName(""), rawEntry("refs/a.md", "0", data))).digest).toBe(await expected()); + }); + + it("keeps PAX records in a prototype-free object", () => { + const { records } = parsePax(paxData({ ["__proto__"]: "x", constructor: "y", path: "p" })); + expect(Object.getPrototypeOf(records)).toBeNull(); + expect(Object.keys(records).sort()).toEqual(["__proto__", "constructor", "path"]); + expect(records["__proto__"]).toBe("x"); + }); +}); + +describe("tar reader bounds bytes that no file limit counts", () => { + const limits = { maxEntries: 10, maxFiles: 10, maxFileSize: 1 << 20, maxTreeSize: 2 << 20 }; + + async function readCounted(source: AsyncIterable): Promise<{ code: string | null; pulled: number }> { + let pulled = 0; + const counted = (async function* () { + for await (const chunk of source) { + pulled += chunk.length; + yield chunk; + } + })(); + const code = await errorCodeOf(() => readTarTree(counted, { limits })); + return { code, pulled }; + } + + it("stops in the data of a non-regular entry", async () => { + const got = await readCounted(tarStream([skill, { name: enc.encode("big.bin"), typeflag: "7", mode: 0o644, fill: { size: 64 << 20, byte: 0 } }])); + expect(got.code).toBe("tree.limit_exceeded"); + expect(got.pulled).toBeLessThan(4 << 20); + }); + + it("stops in a chain of PAX headers", async () => { + const chain = (async function* () { + yield* tarStream([skill], false); + const header = rawEntry("PaxHeader", "x", paxData({ comment: "c".repeat(60_000) })); + for (let i = 0; i < 100_000; i++) yield header; + })(); + const got = await readCounted(chain); + expect(got.code).toBe("tree.limit_exceeded"); + expect(got.pulled).toBeLessThan(4 << 20); + }); +}); + +describe("tar reader handles GNU sparse files like Go", () => { + const sparse = (pax: Record, data: Uint8Array | string, name = "data.bin"): TarSpec => ({ + name: enc.encode(name), + typeflag: "0", + mode: 0o644, + data: typeof data === "string" ? enc.encode(data) : data, + pax, + }); + const v01 = (numblocks: string, map: string) => ({ "GNU.sparse.major": "0", "GNU.sparse.minor": "1", "GNU.sparse.numblocks": numblocks, "GNU.sparse.map": map }); + + it.each([ + ["1.0 with an empty map", sparse(SPARSE_1_0, sparse1x0Data("x")), "tree.unsupported_entry"], + ["1.0 with a map entry", sparse(SPARSE_1_0, sparse1x0Data("x", "1\n0\n1\n")), "tree.unsupported_entry"], + ["0.1 map", sparse(v01("1", "0,1"), "xy"), "tree.unsupported_entry"], + ["0.0 offset and numbytes records", sparse({ "GNU.sparse.numblocks": "1", "GNU.sparse.offset": "0", "GNU.sparse.numbytes": "1" }, "x"), "tree.unsupported_entry"], + ["unknown version: not sparse, records still unsupported", sparse({ "GNU.sparse.major": "2", "GNU.sparse.minor": "0" }, "x"), "tree.unsupported_entry"], + ["size record wins over realsize", sparse({ ...SPARSE_1_0, "GNU.sparse.size": "1", "GNU.sparse.realsize": String(64 << 20) }, sparse1x0Data("x")), "tree.unsupported_entry"], + ["0.1 map with too few fields", sparse(v01("2", "0,1"), "xy"), "tree.read_failed"], + ["0.1 entry beyond the logical size", sparse(v01("1", "0,5"), "xy"), "tree.read_failed"], + ["0.1 overlapping entries", sparse(v01("2", "0,2,1,1"), "xyz"), "tree.read_failed"], + ["0.1 negative offset", sparse(v01("1", "-1,1"), "xy"), "tree.read_failed"], + ["0.1 missing entry count", sparse({ "GNU.sparse.major": "0", "GNU.sparse.minor": "1", "GNU.sparse.map": "0,1" }, "xy"), "tree.read_failed"], + ["numbytes before offset", sparse({ "GNU.sparse.numblocks": "1", "GNU.sparse.numbytes": "1", "GNU.sparse.offset": "0" }, "x"), "tree.read_failed"], + ["1.0 map that is not decimal", sparse(SPARSE_1_0, sparse1x0Data("x", "1\nzero\n1\n")), "tree.read_failed"], + ["1.0 map past the data section", sparse(SPARSE_1_0, "1\n"), "tree.read_failed"], + ["invalid GNU.sparse.size", sparse({ ...SPARSE_1_0, "GNU.sparse.size": "x" }, sparse1x0Data("x")), "tree.read_failed"], + ["negative GNU.sparse.size", sparse({ ...SPARSE_1_0, "GNU.sparse.size": "-1" }, sparse1x0Data("x")), "tree.read_failed"], + ] as const)("%s", async (_name, spec, want) => { + expect(await tarCode([skill, spec])).toBe(want); + }); + + it("applies GNU.sparse.name before the noise rules", async () => { + const got = await readTarTree(tarStream([skill, sparse({ ...SPARSE_1_0, "GNU.sparse.name": ".DS_Store" }, sparse1x0Data("x"))])); + expect(got.digest).toBe((await readTarTree(tarStream([skill]))).digest); + }); + + it("counts the logical size toward the limits", async () => { + expect(await tarCode([skill, sparse({ ...SPARSE_1_0, "GNU.sparse.realsize": String(64 << 20) }, sparse1x0Data("x"))])).toBe("tree.limit_exceeded"); + }); + + describe("old GNU sparse headers", () => { + const data = enc.encode("xy"); + const gnu: Array<[number, string | number[]]> = [[257, "ustar \0"], [483, "00000000002\0"]]; + const code = (...parts: Uint8Array[]) => errorCodeOf(async () => readTarTree(concat([await skillBytes(), ...parts, END]))); + + it("are rejected outside the GNU format", async () => { + expect(await code(rawEntry("data.bin", "S", data))).toBe("tree.read_failed"); + }); + + it("are unsupported with a valid map", async () => { + expect(await code(rawEntry("data.bin", "S", data, [...gnu, [386, "00000000000\0"], [398, "00000000001\0"]]))).toBe("tree.unsupported_entry"); + }); + + it("fail with a map beyond the real size", async () => { + expect(await code(rawEntry("data.bin", "S", data, [...gnu, [386, "00000000000\0"], [398, "00000000005\0"]]))).toBe("tree.read_failed"); + }); + + it("read extension blocks before the data", async () => { + const ext = new Uint8Array(512); + ext.set(enc.encode("00000000001\0"), 0); + ext.set(enc.encode("00000000001\0"), 12); + const header = patchHeader(tarHeader(enc.encode("data.bin"), "S", 0o644, data.length), [...gnu, [482, [1]]]); + expect(await code(concat([header, ext, data, pad(data.length)]), rawEntry("refs/a.md", "0", enc.encode("x\n")))).toBe("tree.unsupported_entry"); + ext.set(enc.encode("00000000009\0"), 12); + expect(await code(concat([header, ext, data, pad(data.length)]))).toBe("tree.read_failed"); + }); + }); +}); diff --git a/packages/skill-tree/test/tree-rules.test.ts b/packages/skill-tree/test/tree-rules.test.ts new file mode 100644 index 0000000..7d52d50 --- /dev/null +++ b/packages/skill-tree/test/tree-rules.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { LIMITS, buildTree, checkLimits, headerMode } from "../src/tree"; +import type { TreeEntry } from "../src/tree"; +import { errorCodeOf } from "./vector-helpers"; + +describe("headerMode", () => { + it.each([ + [0o100644, "100644"], + [0o644, "100644"], + [0o100755, "100755"], + [0o170644, "100644"], // type bits are cleared, as Go's &^ 0o170000 + ])("maps %o to a mode class", (mode, want) => { + expect(headerMode(mode)).toBe(want); + }); + + it.each([ + [2 ** 32 + 0o644, "040000000644"], + [2 ** 32 + 0o100755, "040000100755"], + [0o7777777 + 1, "010000000"], + [-1, "0-1"], + [-(2 ** 32) + 0o644, "0-37777777134"], + [1.5, "01.4"], + [Number.MAX_SAFE_INTEGER + 1, `0${(2 ** 53).toString(8)}`], + [NaN, "0NaN"], + [Infinity, "0Infinity"], + ])("rejects %s without truncating it", (mode, want) => { + expect(headerMode(mode)).toBe(want); + }); +}); + +describe("checkLimits", () => { + const file = (size: number): TreeEntry => ({ path: "a.md", mode: "100644", size, sha256: "0".repeat(64) }); + + it.each([NaN, 1.5, Infinity, 2 ** 53])("rejects size %s as tree.limit_exceeded", (size) => { + expect(checkLimits([file(size)], 1, LIMITS)?.code).toBe("tree.limit_exceeded"); + }); + + it("lets validation reject a negative size", async () => { + expect(checkLimits([file(-1)], 1, LIMITS)).toBeNull(); + expect(await errorCodeOf(() => buildTree([file(-1)]))).toBe("tree.unsupported_entry"); + }); +}); diff --git a/packages/skill-tree/test/tree-vectors.test.ts b/packages/skill-tree/test/tree-vectors.test.ts index fd8ceef..a3a17a1 100644 --- a/packages/skill-tree/test/tree-vectors.test.ts +++ b/packages/skill-tree/test/tree-vectors.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { SkillTreeError, buildTree, readTarTree, readZipTree, serializeTree } from "../src/index"; import type { TreeManifest } from "../src/index"; -import { tarStream } from "./tar-writer"; +import { concat, tarStream } from "./tar-writer"; import type { TarSpec } from "./tar-writer"; import { zipBytes } from "./zip-writer"; import type { ZipSpec } from "./zip-writer"; @@ -11,6 +11,11 @@ import type { TreeVector, VectorFile } from "./vector-helpers"; const doc = loadTreeVectors(); const vectors = doc.vectors; +/** Vectors each archive harness presents today; lower them only with a reason. */ +const MIN_TAR_VECTORS = 133; +const MIN_ZIP_VECTORS = 113; +const ran = { tar: 0, zip: 0 }; + const VECTOR_KEYS = new Set(["id", "description", "files", "synthesize", "archiveOnly", "expect"]); const FILE_KEYS = new Set(["path", "rawPathBase64", "mode", "headerMode", "type", "paxRecords", "content", "contentBase64"]); const SYNTH_KEYS = new Set(["pathPattern", "count", "size", "fill", "mode", "type"]); @@ -42,6 +47,15 @@ const TAR_TYPES: Record = { }; const HEADER_ONLY = new Set(["1", "2", "3", "4", "5", "6"]); +/** An empty GNU 1.0 sparse map ("0\n" in one block), which Go reads from the start of the data section. */ +const EMPTY_SPARSE_1_0_MAP = (() => { + const block = new Uint8Array(512); + block.set(new TextEncoder().encode("0\n")); + return block; +})(); + +const isSparse1x0 = (pax?: Record) => pax?.["GNU.sparse.major"] === "1" && pax["GNU.sparse.minor"] === "0"; + /** The vector as a tar stream, or undefined when tar cannot present it (walker-only types, NUL names). */ function tarSpecs(v: TreeVector): TarSpec[] | undefined { const specs: TarSpec[] = []; @@ -51,7 +65,9 @@ function tarSpecs(v: TreeVector): TarSpec[] | undefined { if (typeflag === undefined || name.includes(0) || (typeflag !== "g" && f.paxRecords?.path !== undefined)) return undefined; const pax = f.paxRecords; // Header-only types (links, devices, directories, FIFOs) carry no data, as Go's tar writer enforces. - const data = typeflag === "g" || HEADER_ONLY.has(typeflag) ? undefined : fileBytes(f); + let data = typeflag === "g" || HEADER_ONLY.has(typeflag) ? undefined : fileBytes(f); + // A GNU 1.0 sparse file's data section starts with its map; without one Go's reader fails. + if (data !== undefined && typeflag !== "g" && isSparse1x0(pax)) data = concat([EMPTY_SPARSE_1_0_MAP, data]); specs.push({ name, typeflag, mode: archiveMode(f), data, ...(pax ? { pax } : {}) } as TarSpec); } for (const s of v.synthesize ?? []) { @@ -117,11 +133,20 @@ describe("skill-tree-v1 vectors (vendored unedited from AgentKit)", () => { const specs = tarSpecs(v); if (specs === undefined) return; expectOutcome(await outcome(() => readTarTree(tarStream(specs))), v); + ran.tar++; }, 120_000); it.each(vectors.map((v) => [v.id, v] as const))("zip reader agrees: %s", async (_id, v) => { const specs = zipSpecs(v); if (specs === undefined) return; expectOutcome(await outcome(() => readZipTree(zipBytes(specs))), v); + ran.zip++; }, 60_000); + + // Tests in a file run in order, so this sees every reader run above; it + // stops a harness change from silently skipping most vectors. + it("each archive reader ran most vectors", () => { + expect(ran.tar).toBeGreaterThanOrEqual(MIN_TAR_VECTORS); + expect(ran.zip).toBeGreaterThanOrEqual(MIN_ZIP_VECTORS); + }); }); diff --git a/packages/skill-tree/test/zip-reader.test.ts b/packages/skill-tree/test/zip-reader.test.ts index d626f60..21a4d07 100644 --- a/packages/skill-tree/test/zip-reader.test.ts +++ b/packages/skill-tree/test/zip-reader.test.ts @@ -63,6 +63,25 @@ describe("zip reader rejects unsafe or unsupported entries", () => { expect(await errorCodeOf(() => readZipTree(bytes))).toBe("tree.read_failed"); }); + it("never inflates an encrypted deflate entry", async () => { + const bytes = zipBytes([skill, entry("a.md", "z".repeat(4096), { method: 8, flags: 0x1 })]); + const local = Buffer.from(bytes).indexOf("a.md") + 4; + bytes.fill(0xff, local, local + 6); // ciphertext is not a deflate stream + expect(await errorCodeOf(() => readZipTree(bytes))).toBe("tree.unsupported_entry"); + }); + + it("counts unsupported entries toward no file limit", async () => { + const files = Array.from({ length: 4095 }, (_, n) => entry(`f/${n}.md`)); + expect(await zipCode([skill, ...files, entry("link", "/etc", { unixMode: 0o120777 })])).toBe("tree.unsupported_entry"); + expect(await zipCode([skill, ...files, entry("extra.md")])).toBe("tree.limit_exceeded"); + }); + + it("does not count a faulted entry's data toward the byte limit", async () => { + const limits = { maxEntries: 10, maxFiles: 10, maxFileSize: 1024, maxTreeSize: 4096 }; + const bytes = zipBytes([skill, entry("big.bin", "0".repeat(1 << 20), { method: 8, centralCrc: 0x12345678 })]); + expect(await errorCodeOf(() => readZipTree(bytes, { limits }))).toBe("tree.unsupported_entry"); + }); + it("rejects deflate data that is corrupt", async () => { const bytes = zipBytes([skill, entry("a.md", "z".repeat(4096), { method: 8 })]); const local = Buffer.from(bytes).indexOf("a.md") + 4; From dd28390dedb3ab3a32e907bed2f9c092bfd23f4b Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 11:04:04 +0700 Subject: [PATCH 04/14] fix(skill-tree): normalize tar type NUL and scope the ingest overhead guard Rewrite type NUL from the merged name before sparse handling, as Go Reader.Next does, and use that type for the data length, the sparse header-only check and the entry kind. Check the raw header size for negativity before a PAX size overrides it. The tar byte budget now counts only bytes that are not hashed file content, at maxFiles x 64 KiB, and its detail names the ingest overhead guard. Document the guard and the nodejs_compat requirement. --- packages/skill-tree/README.md | 30 +++++++++++ packages/skill-tree/src/byte-source.ts | 22 ++++++-- packages/skill-tree/src/tar-reader.ts | 35 +++++++----- packages/skill-tree/src/tar-sparse.ts | 4 +- packages/skill-tree/test/tar-reader.test.ts | 59 ++++++++++++++++++--- 5 files changed, 124 insertions(+), 26 deletions(-) create mode 100644 packages/skill-tree/README.md diff --git a/packages/skill-tree/README.md b/packages/skill-tree/README.md new file mode 100644 index 0000000..06276b3 --- /dev/null +++ b/packages/skill-tree/README.md @@ -0,0 +1,30 @@ +# @skillx/skill-tree + +TypeScript port of the AgentKit `agentkit.skill-tree/v1` digest. It reads a tar, +tar.gz or zip archive (or presented entries) into a validated tree manifest +and its digest. Go parity is pinned by the vendored conformance vectors under +`fixtures/`. + +## Runtime requirements + +- Node.js 21.2 or later (`DecompressionStream("deflate-raw")`). +- Cloudflare Workers: enable the `nodejs_compat` compatibility flag. The + package imports `node:crypto` (`createHash`) for SHA-256. + +## Ingest overhead guard (tar) + +The tar reader caps the bytes it reads beyond hashed regular-file content at +`maxFiles × 64 KiB` (256 MiB at the default limits). That overhead covers +headers, PAX `x`/`g` and GNU `L`/`K` records, sparse maps, padding and the data +of non-regular entries. Regular-file content is bounded by the tree limits +(`maxFileSize`, `maxTreeSize`, `maxFiles`) as before. + +- It only rejects (`tree.limit_exceeded`, detail naming the ingest overhead + guard). It never changes a digest. +- It is a SkillX resource guard outside the Go parity claim. Go + `archive/tar` has no such bound, and AgentKit has no tar reader; the + contract governs `Build`. +- Known divergence: a non-regular entry (for example type `7` or a sparse + file) whose data exceeds the guard fails with `tree.limit_exceeded`, where + Go `Reader.Next` plus `Build` would read it fully and report + `tree.unsupported_entry`. diff --git a/packages/skill-tree/src/byte-source.ts b/packages/skill-tree/src/byte-source.ts index 893444a..f24d0f3 100644 --- a/packages/skill-tree/src/byte-source.ts +++ b/packages/skill-tree/src/byte-source.ts @@ -100,7 +100,11 @@ export class StreamReader { private ended = false; private consumed = 0; - /** `budget` caps the total bytes taken from the stream: headers, skipped data and file content alike. */ + /** + * `budget` is the ingest overhead guard: it caps the bytes taken from the + * stream that are not hashed file content (headers, extension records, + * skipped data). Hashed content is bounded by the tree limits instead. + */ constructor(source: AsyncIterable, private readonly budget = Infinity) { this.it = source[Symbol.asyncIterator](); } @@ -108,7 +112,11 @@ export class StreamReader { private spend(n: number): void { this.consumed += n; if (this.consumed > this.budget) { - throw new SkillTreeError(ERROR_CODES.limitExceeded, "", `archive data exceeds ${this.budget} bytes`); + throw new SkillTreeError( + ERROR_CODES.limitExceeded, + "", + `archive overhead exceeds the ${this.budget}-byte ingest overhead guard`, + ); } } @@ -149,15 +157,19 @@ export class StreamReader { return out; } - /** Yields exactly `n` bytes in chunks; throws `tree.read_failed` if the stream ends first. */ - async *chunks(n: number): AsyncGenerator { + /** + * Yields exactly `n` bytes in chunks; throws `tree.read_failed` if the + * stream ends first. `overhead` false exempts content the caller hashes + * and counts against the tree limits from the overhead guard. + */ + async *chunks(n: number, overhead = true): AsyncGenerator { let left = n; while (left > 0) { if (this.offset >= this.chunk.length && !(await this.nextChunk())) { throw readFailed(`stream ends ${left} bytes before the end of an entry`); } const take = Math.min(left, this.chunk.length - this.offset); - this.spend(take); + if (overhead) this.spend(take); const piece = this.chunk.subarray(this.offset, this.offset + take); this.offset += take; left -= take; diff --git a/packages/skill-tree/src/tar-reader.ts b/packages/skill-tree/src/tar-reader.ts index 551ee72..048ebc5 100644 --- a/packages/skill-tree/src/tar-reader.ts +++ b/packages/skill-tree/src/tar-reader.ts @@ -91,16 +91,19 @@ function dataLength(typeflag: string, size: number): number { return n; } -/** Bytes an archive may take beyond its file content: headers, extension records and skipped data. */ -const ENTRY_OVERHEAD = 16 * 1024; +/** Overhead allowance per file (of `maxFiles`) for the ingest overhead guard. */ +const ENTRY_OVERHEAD = 64 * 1024; /** - * The total byte budget of one archive. Go archive/tar has no such limit; - * it bounds data that no file limit counts (non-regular entry data, PAX and - * GNU extension chains), so a stream cannot be read without end. + * The ingest overhead guard of one archive: the bytes it may take beyond + * hashed regular-file content (headers, PAX/GNU extension records, sparse + * maps, padding and the data of non-regular entries). Hashed content counts + * against the tree limits instead. This is a SkillX resource guard outside + * the Go parity claim (Go archive/tar has no such bound): it only ever + * rejects with `tree.limit_exceeded` and never changes a digest. */ -export function tarByteBudget(limits: TreeLimits): number { - return limits.maxTreeSize + limits.maxEntries * ENTRY_OVERHEAD; +export function tarOverheadBudget(limits: TreeLimits): number { + return limits.maxFiles * ENTRY_OVERHEAD; } async function readEntries(reader: StreamReader, collector: EntryCollector): Promise { @@ -142,15 +145,20 @@ async function readEntries(reader: StreamReader, collector: EntryCollector): Pro longName = undefined; continue; } + // Go checks the raw header size before any PAX record can override it. + dataLength(h.typeflag, h.size); const records = pax?.records ?? (Object.create(null) as Record); const size = mergePaxValues(records) ?? h.size; let rawName = longName ?? (records.path ? pax!.raw.get("path")! : h.name); - const physical = dataLength(h.typeflag, size); + // Go rewrites TypeRegA ('\0') from the merged name, before sparse handling: + // a directory when it ends in "/", a regular file otherwise. + const type = h.typeflag === "\0" ? (rawName[rawName.length - 1] === 0x2f ? "5" : "0") : h.typeflag; + const physical = dataLength(type, size); let logical = size; let consumed = 0; - if (h.typeflag === "S") await readOldGnuSparse(reader, h); + if (type === "S") await readOldGnuSparse(reader, h); else { - const sparse = await readPaxSparse(reader, h, pax, size, physical); + const sparse = await readPaxSparse(reader, type, pax, size, physical); if (sparse !== undefined) { rawName = sparse.name ?? rawName; logical = sparse.size; @@ -158,10 +166,11 @@ async function readEntries(reader: StreamReader, collector: EntryCollector): Pro } } const name = decodeName(rawName); - const kind = tarEntryKind(h.typeflag, name.path); + // The kind follows the effective type, never the sparse-overridden name. + const kind = tarEntryKind(type, name.path); const sparse = logical !== size || consumed > 0 || Object.keys(records).some((k) => k.startsWith("GNU.sparse.")); let hashed = { size: 0, sha256: "" }; - if (kind === "regular" && !sparse) hashed = await collector.hashFile(name.path, reader.chunks(physical)); + if (kind === "regular" && !sparse) hashed = await collector.hashFile(name.path, reader.chunks(physical, false)); else { // Sparse files are rejected later, but their logical size counts toward the limits first. if (kind === "regular") { @@ -187,7 +196,7 @@ export async function readTarTree(source: ByteSource, options: TarReadOptions = const collector = new EntryCollector(limits); let reader: StreamReader | undefined; try { - reader = new StreamReader(await gunzipIfNeeded(toAsyncIterable(source), options.compression ?? "auto"), tarByteBudget(limits)); + reader = new StreamReader(await gunzipIfNeeded(toAsyncIterable(source), options.compression ?? "auto"), tarOverheadBudget(limits)); await readEntries(reader, collector); } catch (err) { throw asTreeError(err); diff --git a/packages/skill-tree/src/tar-sparse.ts b/packages/skill-tree/src/tar-sparse.ts index 24ad96f..a4b9cff 100644 --- a/packages/skill-tree/src/tar-sparse.ts +++ b/packages/skill-tree/src/tar-sparse.ts @@ -116,7 +116,7 @@ export interface PaxSparse { */ export async function readPaxSparse( reader: StreamReader, - h: RawHeader, + typeflag: string, pax: PaxRecords | undefined, size: number, physical: number, @@ -133,7 +133,7 @@ export async function readPaxSparse( logical = n; } const { entries, consumed } = version === "1.0" ? await map1x0(reader, physical) : { entries: map0x1(records), consumed: 0 }; - if (isHeaderOnly(h.typeflag) || !validSparseEntries(entries, logical)) throw readFailed("invalid GNU sparse map"); + if (isHeaderOnly(typeflag) || !validSparseEntries(entries, logical)) throw readFailed("invalid GNU sparse map"); return { ...(nameText === "" ? {} : { name: pax?.raw.get("GNU.sparse.name") }), size: Number(logical), consumed }; } diff --git a/packages/skill-tree/test/tar-reader.test.ts b/packages/skill-tree/test/tar-reader.test.ts index 1f9b0a7..7852dd1 100644 --- a/packages/skill-tree/test/tar-reader.test.ts +++ b/packages/skill-tree/test/tar-reader.test.ts @@ -212,6 +212,13 @@ describe("tar headers parse like Go archive/tar", () => { expect(await code(rawEntry("refs/", "5", new Uint8Array(0), [[124, ff(12)]]), rawEntry("refs/a.md", "0", data))).toBeNull(); }); + it("checks the raw header size before a PAX size overrides it", async () => { + const paxSize = (size: string) => rawEntry("PaxHeader", "x", paxData({ size })); + expect(await code(paxSize("2"), rawEntry("refs/a.md", "0", data, [[124, ff(12)]]))).toBe("tree.read_failed"); + // A negative merged size is fine once type NUL becomes a header-only directory. + expect(await code(paxSize("-1"), rawEntry("refs/", "\0", new Uint8Array(0)), rawEntry("refs/a.md", "0", data))).toBeNull(); + }); + it("never truncates a base-256 mode to 32 bits", async () => { const mode = [0x80, 0, 0, 0x01, 0, 0, 0x01, 0xa4]; // 2^32 + 0o644 expect(await code(rawEntry("refs/a.md", "0", data, [[100, mode]]))).toBe("tree.unsupported_entry"); @@ -261,10 +268,31 @@ describe("tar headers parse like Go archive/tar", () => { }); }); -describe("tar reader bounds bytes that no file limit counts", () => { +describe("tar reader rewrites type NUL like Go archive/tar", () => { + const data = enc.encode("x\n"); + const v01 = { "GNU.sparse.major": "0", "GNU.sparse.minor": "1", "GNU.sparse.numblocks": "0", "GNU.sparse.map": "" }; + + it("reads no data for a type NUL directory, so the next block is a header", async () => { + const smuggled = rawEntry("evil.md", "0", data); + const manifest = await readTarTree(concat([await skillBytes(), rawEntry("refs/", "\0", smuggled), END])); + expect(manifest.files.map((f) => f.path)).toEqual(["SKILL.md", "evil.md"]); + expect(manifest.digest).toBe((await readTarTree(await tarBytes([skill, file("evil.md")]))).digest); + }); + + it("rejects a sparse map on a type NUL directory", async () => { + expect(await tarCode([skill, { name: enc.encode("refs/"), typeflag: "\0", mode: 0o755, pax: v01 }])).toBe("tree.read_failed"); + }); + + it("decides the type from the name before GNU.sparse.name replaces it", async () => { + const spec = { name: enc.encode("placeholder"), typeflag: "\0", mode: 0o644, data: enc.encode("x"), pax: { ...v01, path: "a", "GNU.sparse.name": "a/" } }; + expect(await tarCode([skill, spec])).toBe("tree.unsupported_entry"); + }); +}); + +describe("tar reader ingest overhead guard", () => { const limits = { maxEntries: 10, maxFiles: 10, maxFileSize: 1 << 20, maxTreeSize: 2 << 20 }; - async function readCounted(source: AsyncIterable): Promise<{ code: string | null; pulled: number }> { + async function readCounted(source: AsyncIterable): Promise<{ error: unknown; pulled: number }> { let pulled = 0; const counted = (async function* () { for await (const chunk of source) { @@ -272,13 +300,18 @@ describe("tar reader bounds bytes that no file limit counts", () => { yield chunk; } })(); - const code = await errorCodeOf(() => readTarTree(counted, { limits })); - return { code, pulled }; + const error = await readTarTree(counted, { limits }).then( + () => null, + (err: unknown) => err, + ); + return { error, pulled }; } + const overheadGuard = expect.objectContaining({ code: "tree.limit_exceeded", detail: expect.stringContaining("ingest overhead guard") }); + it("stops in the data of a non-regular entry", async () => { const got = await readCounted(tarStream([skill, { name: enc.encode("big.bin"), typeflag: "7", mode: 0o644, fill: { size: 64 << 20, byte: 0 } }])); - expect(got.code).toBe("tree.limit_exceeded"); + expect(got.error).toEqual(overheadGuard); expect(got.pulled).toBeLessThan(4 << 20); }); @@ -289,9 +322,23 @@ describe("tar reader bounds bytes that no file limit counts", () => { for (let i = 0; i < 100_000; i++) yield header; })(); const got = await readCounted(chain); - expect(got.code).toBe("tree.limit_exceeded"); + expect(got.error).toEqual(overheadGuard); expect(got.pulled).toBeLessThan(4 << 20); }); + + it("accepts a near-limit tree with a 40 KiB xattr on every file at the default limits", async () => { + // 4096 files (the file limit) totalling exactly 256 MiB (the tree limit), + // each behind a PAX header with a 40 KiB SCHILY.xattr record: about 170 MiB + // of overhead on top of the content. Specs share one buffer and stream lazily. + const content = new Uint8Array(64 << 10).fill(0x61); + const pax = { "SCHILY.xattr.user.blob": "x".repeat(40 << 10) }; + const specs: TarSpec[] = [{ ...file("SKILL.md", SKILL), pax }]; + specs.push({ ...file("f/0000.md", new Uint8Array((128 << 10) - SKILL.length).fill(0x62)), pax }); + for (let n = 1; n < 4095; n++) specs.push({ ...file(`f/${String(n).padStart(4, "0")}.md`, content), pax }); + const manifest = await readTarTree(tarStream(specs)); + expect(manifest.files).toHaveLength(4096); + expect(manifest.files.reduce((sum, f) => sum + f.size, 0)).toBe(256 << 20); + }, 300_000); }); describe("tar reader handles GNU sparse files like Go", () => { From 653b61c1846eaeb29267a31502922342d69909e2 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 11:09:33 +0700 Subject: [PATCH 05/14] test(skill-tree): cover type NUL directories named by merged metadata Pin that a type NUL entry whose trailing slash comes from a PAX path or a GNU long name reads no data, and keep the vector helper from writing data for a type NUL directory. --- packages/skill-tree/test/tar-reader.test.ts | 9 +++++++++ packages/skill-tree/test/tree-vectors.test.ts | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/skill-tree/test/tar-reader.test.ts b/packages/skill-tree/test/tar-reader.test.ts index 7852dd1..45f4880 100644 --- a/packages/skill-tree/test/tar-reader.test.ts +++ b/packages/skill-tree/test/tar-reader.test.ts @@ -279,6 +279,15 @@ describe("tar reader rewrites type NUL like Go archive/tar", () => { expect(manifest.digest).toBe((await readTarTree(await tarBytes([skill, file("evil.md")]))).digest); }); + it.each([ + ["a PAX path", () => rawEntry("PaxHeader", "x", paxData({ path: "refs/" }))], + ["a GNU long name", () => rawEntry("././@LongLink", "L", enc.encode("refs/\0"))], + ] as const)("takes the directory slash from %s, not the raw header name", async (_name, meta) => { + const smuggled = rawEntry("evil.md", "0", data); + const manifest = await readTarTree(concat([await skillBytes(), meta(), rawEntry("refs", "\0", smuggled), END])); + expect(manifest.files.map((f) => f.path)).toEqual(["SKILL.md", "evil.md"]); + }); + it("rejects a sparse map on a type NUL directory", async () => { expect(await tarCode([skill, { name: enc.encode("refs/"), typeflag: "\0", mode: 0o755, pax: v01 }])).toBe("tree.read_failed"); }); diff --git a/packages/skill-tree/test/tree-vectors.test.ts b/packages/skill-tree/test/tree-vectors.test.ts index a3a17a1..c32e9ec 100644 --- a/packages/skill-tree/test/tree-vectors.test.ts +++ b/packages/skill-tree/test/tree-vectors.test.ts @@ -65,7 +65,9 @@ function tarSpecs(v: TreeVector): TarSpec[] | undefined { if (typeflag === undefined || name.includes(0) || (typeflag !== "g" && f.paxRecords?.path !== undefined)) return undefined; const pax = f.paxRecords; // Header-only types (links, devices, directories, FIFOs) carry no data, as Go's tar writer enforces. - let data = typeflag === "g" || HEADER_ONLY.has(typeflag) ? undefined : fileBytes(f); + // Type NUL with a name ending in "/" is a directory to Go's reader, so it is header-only too. + const nulDir = typeflag === "\0" && name[name.length - 1] === 0x2f; + let data = typeflag === "g" || nulDir || HEADER_ONLY.has(typeflag) ? undefined : fileBytes(f); // A GNU 1.0 sparse file's data section starts with its map; without one Go's reader fails. if (data !== undefined && typeflag !== "g" && isSparse1x0(pax)) data = concat([EMPTY_SPARSE_1_0_MAP, data]); specs.push({ name, typeflag, mode: archiveMode(f), data, ...(pax ? { pax } : {}) } as TarSpec); From 236f951e6982117ccb2c129ee702829d2e928299 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 11:50:56 +0700 Subject: [PATCH 06/14] feat(skill-tree): add a rules entry point without Node imports Export the path, collision and identity rules from @skillx/skill-tree/rules so Worker code can validate manifests without pulling node:crypto or the archive readers into its program. --- packages/skill-tree/README.md | 3 +++ packages/skill-tree/package.json | 3 ++- packages/skill-tree/src/rules.ts | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 packages/skill-tree/src/rules.ts diff --git a/packages/skill-tree/README.md b/packages/skill-tree/README.md index 06276b3..3c57358 100644 --- a/packages/skill-tree/README.md +++ b/packages/skill-tree/README.md @@ -10,6 +10,9 @@ and its digest. Go parity is pinned by the vendored conformance vectors under - Node.js 21.2 or later (`DecompressionStream("deflate-raw")`). - Cloudflare Workers: enable the `nodejs_compat` compatibility flag. The package imports `node:crypto` (`createHash`) for SHA-256. +- `@skillx/skill-tree/rules` exports only the pure path, collision and + identity rules (no Node.js import, no archive readers). Worker and browser + code that validates manifests, such as `@skillx/contracts`, imports it. ## Ingest overhead guard (tar) diff --git a/packages/skill-tree/package.json b/packages/skill-tree/package.json index 4fc376b..7d3fd40 100644 --- a/packages/skill-tree/package.json +++ b/packages/skill-tree/package.json @@ -5,7 +5,8 @@ "type": "module", "description": "agentkit.skill-tree/v1 canonical tree digest, archive readers, identity and exposure rules shared with AgentKit.", "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./rules": "./src/rules.ts" }, "types": "./src/index.ts", "engines": { diff --git a/packages/skill-tree/src/rules.ts b/packages/skill-tree/src/rules.ts new file mode 100644 index 0000000..8f60454 --- /dev/null +++ b/packages/skill-tree/src/rules.ts @@ -0,0 +1,15 @@ +/** + * `@skillx/skill-tree/rules`: the pure validation rules of + * `agentkit.skill-tree/v1` and package identity, with no Node.js or archive + * dependency. Worker and browser code (such as `@skillx/contracts`) imports + * this entry point instead of the package root, which also pulls in hashing + * and the tar and zip readers. + */ +export { ERROR_CODES, SkillTreeError, isSkillTreeError } from "./errors"; +export type { SkillTreeErrorCode } from "./errors"; + +export { isNoisePath, validatePath } from "./path-rules"; +export { checkCollisions, foldKey } from "./collision"; +export { UNICODE_VERSION } from "./unicode-15"; + +export { IDENTITY_PATTERN, RESERVED_PUBLISHER, checkPublisher, validateIdentity } from "./identity"; From f3f29a21906fba2f587c0a36112bc06bc412b0a4 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 11:50:56 +0700 Subject: [PATCH 07/14] feat(contracts): add the signed skillx.release/v1 manifest contract Define the release manifest that pins a package version to its agentkit.skill-tree/v1 tree digest, a strict validator, byte-exact canonical JSON for the signing payload, and WebCrypto Ed25519 signing and fail-closed verification compatible with the AgentKit registry style. Ship cross-language vectors signed with a key derived from a public seed text, plus a skill-tree test that recomputes each vector's tree digest. --- packages/contracts/package.json | 3 + packages/contracts/src/canonical-json.test.ts | 66 + packages/contracts/src/canonical-json.ts | 97 + packages/contracts/src/fixtures/index.ts | 3 + .../src/fixtures/release-fixtures.ts | 214 ++ .../src/fixtures/release-manifest-v1.json | 1865 +++++++++++++++++ packages/contracts/src/fixtures/types.ts | 3 +- packages/contracts/src/index.ts | 2 + .../contracts/src/package-manifest.test.ts | 9 +- .../contracts/src/release-manifest.test.ts | 212 ++ packages/contracts/src/release-manifest.ts | 490 +++++ .../contracts/src/release-vectors.test.ts | 204 ++ packages/contracts/tsconfig.json | 5 +- .../test/release-manifest-vectors.test.ts | 46 + pnpm-lock.yaml | 4 + 15 files changed, 3218 insertions(+), 5 deletions(-) create mode 100644 packages/contracts/src/canonical-json.test.ts create mode 100644 packages/contracts/src/canonical-json.ts create mode 100644 packages/contracts/src/fixtures/release-fixtures.ts create mode 100644 packages/contracts/src/fixtures/release-manifest-v1.json create mode 100644 packages/contracts/src/release-manifest.test.ts create mode 100644 packages/contracts/src/release-manifest.ts create mode 100644 packages/contracts/src/release-vectors.test.ts create mode 100644 packages/skill-tree/test/release-manifest-vectors.test.ts diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 560b5c2..b037eac 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -11,6 +11,9 @@ "scripts": { "typecheck": "tsc --noEmit" }, + "dependencies": { + "@skillx/skill-tree": "workspace:*" + }, "devDependencies": { "typescript": "^5.9.2" } diff --git a/packages/contracts/src/canonical-json.test.ts b/packages/contracts/src/canonical-json.test.ts new file mode 100644 index 0000000..42ffc23 --- /dev/null +++ b/packages/contracts/src/canonical-json.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; +import { CanonicalJsonError, canonicalJson, canonicalJsonBytes } from "./canonical-json"; + +// Built from code points so no editor or formatter can normalize them away. +const E_ACUTE = String.fromCodePoint(0xe9); +const EMOJI = String.fromCodePoint(0x1f600); +const HALFWIDTH_STOP = String.fromCodePoint(0xff61); +const LINE_SEPARATOR = String.fromCodePoint(0x2028); +const PARAGRAPH_SEPARATOR = String.fromCodePoint(0x2029); +const DEL = String.fromCharCode(0x7f); +const LONE_HIGH = String.fromCharCode(0xd800); +const LONE_LOW = String.fromCharCode(0xdc00); + +describe("canonical JSON", () => { + it("sorts object members at every depth and drops whitespace", () => { + const a = { b: 1, a: [{ z: true, y: null }], c: { e: "x", d: 0 } }; + const b = { c: { d: 0, e: "x" }, a: [{ y: null, z: true }], b: 1 }; + expect(canonicalJson(a)).toBe('{"a":[{"y":null,"z":true}],"b":1,"c":{"d":0,"e":"x"}}'); + expect(canonicalJson(b)).toBe(canonicalJson(a)); + }); + + it("keeps array order", () => { + expect(canonicalJson(["b", "a"])).toBe('["b","a"]'); + }); + + it("orders keys by UTF-16 code units, as RFC 8785 does", () => { + const value = { [E_ACUTE]: 1, z: 2, A: 3, [EMOJI]: 4, [HALFWIDTH_STOP]: 5 }; + expect(canonicalJson(value)).toBe(`{"A":3,"z":2,"${E_ACUTE}":1,"${EMOJI}":4,"${HALFWIDTH_STOP}":5}`); + }); + + it("escapes only quote, backslash and C0 controls", () => { + const value = ['"', "\\", "\b", "\t", "\n", "\f", "\r", String.fromCharCode(0), String.fromCharCode(0x1f)].join(""); + expect(canonicalJson(value)).toBe(String.raw`"\"\\\b\t\n\f\r\u0000\u001f"`); + }); + + it("emits HTML characters, DEL, U+2028, U+2029 and non-ASCII as raw UTF-8", () => { + const value = `&${DEL}${LINE_SEPARATOR}${PARAGRAPH_SEPARATOR}caf${E_ACUTE}${EMOJI}/`; + expect(canonicalJson(value)).toBe(`"${value}"`); + expect([...canonicalJsonBytes(LINE_SEPARATOR)]).toEqual([0x22, 0xe2, 0x80, 0xa8, 0x22]); + }); + + it("writes non-negative safe integers in plain decimal", () => { + expect(canonicalJson([0, 1, Number.MAX_SAFE_INTEGER])).toBe("[0,1,9007199254740991]"); + }); + + it.each([ + ["a fraction", 1.5], + ["a negative number", -1], + ["negative zero", -0], + ["an unsafe integer", 2 ** 53], + ["NaN", Number.NaN], + ["infinity", Number.POSITIVE_INFINITY], + ["a lone high surrogate", LONE_HIGH], + ["a lone low surrogate", `x${LONE_LOW}`], + ["an undefined member", { a: undefined }], + ["a Date", new Date(0)], + ["a Map", new Map()], + ["a bigint", 1n], + ])("rejects %s", (_name, value) => { + expect(() => canonicalJson(value)).toThrow(CanonicalJsonError); + }); + + it("names the offending path", () => { + expect(() => canonicalJson({ files: [{ size: 1.5 }] })).toThrow("files[0].size"); + }); +}); diff --git a/packages/contracts/src/canonical-json.ts b/packages/contracts/src/canonical-json.ts new file mode 100644 index 0000000..aa06a99 --- /dev/null +++ b/packages/contracts/src/canonical-json.ts @@ -0,0 +1,97 @@ +/** + * Canonical JSON used for signed SkillX payloads. + * + * The output is the RFC 8785 (JCS) form restricted to the value space signed + * payloads use, so every byte is defined without a floating-point formatter: + * + * - objects: members sorted by key, comparing UTF-16 code units (equal to + * byte order for the ASCII keys every SkillX schema uses); no whitespace; + * members whose value is `undefined` are rejected, never skipped; + * - arrays: elements in order, no whitespace; + * - numbers: non-negative safe integers (0 .. 2^53-1) only, in plain decimal; + * - strings: `"` and `\` are escaped as `\"` and `\\`; U+0008, U+0009, + * U+000A, U+000C and U+000D as `\b`, `\t`, `\n`, `\f`, `\r`; every other + * code point below U+0020 as `\u00xx` with lowercase hex; every other code + * point, including `/`, `<`, `>`, `&`, U+007F, U+2028 and U+2029, is + * emitted as raw UTF-8. A string with an unpaired surrogate is rejected; + * - `true`, `false` and `null` literally. + * + * The bytes are the UTF-8 encoding of the result, without a BOM or trailing + * newline. Go's `encoding/json.Marshal` does NOT produce this form (it + * HTML-escapes `<>&`, escapes U+2028/U+2029 and keeps struct field order). + */ + +export class CanonicalJsonError extends Error { + constructor(path: string, message: string) { + super(path === "" ? message : `${path}: ${message}`); + this.name = "CanonicalJsonError"; + } +} + +const SHORT_ESCAPES: Readonly> = { + 0x08: "\\b", + 0x09: "\\t", + 0x0a: "\\n", + 0x0c: "\\f", + 0x0d: "\\r", + 0x22: '\\"', + 0x5c: "\\\\", +}; + +function encodeString(value: string, path: string): string { + let out = '"'; + for (let i = 0; i < value.length; i++) { + const unit = value.charCodeAt(i); + if (unit >= 0xd800 && unit <= 0xdbff) { + const next = value.charCodeAt(i + 1); + if (!(next >= 0xdc00 && next <= 0xdfff)) throw new CanonicalJsonError(path, "string holds an unpaired surrogate"); + out += value[i]! + value[i + 1]!; + i++; + continue; + } + if (unit >= 0xdc00 && unit <= 0xdfff) throw new CanonicalJsonError(path, "string holds an unpaired surrogate"); + const short = SHORT_ESCAPES[unit]; + if (short !== undefined) out += short; + else if (unit < 0x20) out += `\\u${unit.toString(16).padStart(4, "0")}`; + else out += value[i]!; + } + return `${out}"`; +} + +function encode(value: unknown, path: string): string { + if (value === null) return "null"; + if (value === true) return "true"; + if (value === false) return "false"; + if (typeof value === "string") return encodeString(value, path); + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) { + throw new CanonicalJsonError(path, `number ${value} is not a non-negative safe integer`); + } + return String(value); + } + if (Array.isArray(value)) { + return `[${value.map((item, i) => encode(item, `${path}[${i}]`)).join(",")}]`; + } + if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) { + const record = value as Record; + // Array.prototype.sort without a comparator orders by UTF-16 code units, as JCS requires. + const keys = Object.keys(record).sort(); + const members = keys.map((key) => { + const memberPath = path === "" ? key : `${path}.${key}`; + if (record[key] === undefined) throw new CanonicalJsonError(memberPath, "value is undefined"); + return `${encodeString(key, memberPath)}:${encode(record[key], memberPath)}`; + }); + return `{${members.join(",")}}`; + } + throw new CanonicalJsonError(path, `unsupported value of type ${typeof value}`); +} + +/** The canonical JSON text of `value`. Throws `CanonicalJsonError` outside the supported value space. */ +export function canonicalJson(value: unknown): string { + return encode(value, ""); +} + +/** The UTF-8 bytes of `canonicalJson(value)`. */ +export function canonicalJsonBytes(value: unknown): Uint8Array { + return new TextEncoder().encode(canonicalJson(value)); +} diff --git a/packages/contracts/src/fixtures/index.ts b/packages/contracts/src/fixtures/index.ts index 05589ca..ecf2d7a 100644 --- a/packages/contracts/src/fixtures/index.ts +++ b/packages/contracts/src/fixtures/index.ts @@ -11,11 +11,13 @@ import { ENTITLEMENT_FIXTURES, INGESTION_FIXTURES, } from "./collection-fixtures"; +import { RELEASE_MANIFEST_FIXTURES } from "./release-fixtures"; import type { FixtureCase } from "./types"; export * from "./types"; export * from "./manifest-fixtures"; export * from "./collection-fixtures"; +export * from "./release-fixtures"; export const ALL_FIXTURES: FixtureCase[] = [ ...PACKAGE_MANIFEST_FIXTURES, @@ -23,4 +25,5 @@ export const ALL_FIXTURES: FixtureCase[] = [ ...COLLECTION_MEMBER_FIXTURES, ...ENTITLEMENT_FIXTURES, ...INGESTION_FIXTURES, + ...RELEASE_MANIFEST_FIXTURES, ]; diff --git a/packages/contracts/src/fixtures/release-fixtures.ts b/packages/contracts/src/fixtures/release-fixtures.ts new file mode 100644 index 0000000..3149540 --- /dev/null +++ b/packages/contracts/src/fixtures/release-fixtures.ts @@ -0,0 +1,214 @@ +/** + * `skillx.release/v1` manifest fixtures. + * + * File contents are part of the fixture so every consumer can recompute the + * per-file hashes and `releaseDigest` (the `agentkit.skill-tree/v1` tree + * digest) instead of trusting the literals. The signed cross-language vectors + * in `release-manifest-v1.json` are built from these manifests. + */ + +import type { SkillxReleaseManifest, UnsignedReleaseManifest } from "../release-manifest"; +import type { FixtureCase, MutableFixture } from "./types"; + +/** Key ID of the public, test-only vector key; never trusted outside tests. */ +export const RELEASE_FIXTURE_KEY_ID = "skillx-test-vector-1"; + +/** A well-formed placeholder signature for structure-only fixtures; it verifies under no key. */ +export const RELEASE_FIXTURE_PLACEHOLDER_SIGNATURE = `${"A".repeat(86)}==`; + +/** + * A path with non-ASCII, `&` and U+2028, which the canonical JSON emits raw + * (Go's encoding/json would escape `&` and U+2028). Built from code points + * so no editor or formatter can normalize it. + */ +export const RELEASE_FIXTURE_UNICODE_PATH = `references/caf${String.fromCodePoint(0xe9)} & notes${String.fromCodePoint(0x2028)}.md`; + +/** Exact file contents of each fixture tree, keyed by path. */ +export const RELEASE_FIXTURE_CONTENTS: Readonly>>> = { + full: { + "SKILL.md": "---\nname: review-guardrails\ndescription: Review guardrails for pull requests.\n---\n# Review guardrails\n", + [RELEASE_FIXTURE_UNICODE_PATH]: "Notes with a line separator path.\n", + "scripts/check.sh": "#!/bin/sh\necho ok\n", + }, + minimal: { + "SKILL.md": "---\nname: tiny\ndescription: Tiny skill.\n---\n", + }, +}; + +/** A published, publisher-verified multi-file release with git provenance and a declared script. */ +export const RELEASE_MANIFEST_FIXTURE: UnsignedReleaseManifest = { + schemaVersion: "skillx.release/v1", + identity: "@publisher/review-guardrails", + version: "1.2.0", + channel: "stable", + kind: "skill", + releaseDigest: "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + artifactDigest: `sha256:${"c".repeat(64)}`, + artifactSize: 10240, + files: [ + { path: "SKILL.md", mode: "100644", sha256: "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", size: 102 }, + { + path: RELEASE_FIXTURE_UNICODE_PATH, + mode: "100644", + sha256: "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + size: 34, + }, + { path: "scripts/check.sh", mode: "100755", sha256: "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", size: 18 }, + ], + fileCount: 3, + treeSize: 154, + state: "published", + publisher: { handle: "publisher", verifiedBy: "github" }, + sourceProvenance: { + repo: "https://github.com/publisher/review-guardrails", + commit: "0123456789abcdef0123456789abcdef01234567", + path: "skills/review-guardrails", + }, + declaredScripts: ["scripts/check.sh"], + createdAt: "2026-10-13T04:30:00Z", + keyId: RELEASE_FIXTURE_KEY_ID, + signatureAlgorithm: "ed25519", +}; + +/** A quarantined, unverified single-file prerelease without provenance: covers `null` and empty arrays. */ +export const RELEASE_MANIFEST_MINIMAL_FIXTURE: UnsignedReleaseManifest = { + schemaVersion: "skillx.release/v1", + identity: "@solo.dev/tiny_skill", + version: "0.1.0-beta.1", + channel: "beta", + kind: "skill", + releaseDigest: "sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544", + artifactDigest: `sha256:${"d".repeat(64)}`, + artifactSize: 1536, + files: [{ path: "SKILL.md", mode: "100644", sha256: "7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d", size: 44 }], + fileCount: 1, + treeSize: 44, + state: "quarantined", + publisher: { handle: "solo.dev", verifiedBy: "none" }, + sourceProvenance: null, + declaredScripts: [], + createdAt: "2026-10-13T05:00:00Z", + keyId: RELEASE_FIXTURE_KEY_ID, + signatureAlgorithm: "ed25519", +}; + +/** A deep, mutable copy of the full fixture with a placeholder signature. */ +export function signedReleaseDraft(base: UnsignedReleaseManifest = RELEASE_MANIFEST_FIXTURE): MutableFixture { + const copy = JSON.parse(JSON.stringify(base)) as SkillxReleaseManifest; + copy.signature = RELEASE_FIXTURE_PLACEHOLDER_SIGNATURE; + return copy as unknown as MutableFixture; +} + +type Draft = MutableFixture & { + files: Array>; + publisher: Record; + sourceProvenance: Record; +}; + +function release(mutate: (draft: Draft) => void): MutableFixture { + const draft = signedReleaseDraft() as Draft; + mutate(draft); + return draft; +} + +function rejected(name: string, expectedIssueCode: string, mutate: (draft: Draft) => void): FixtureCase { + return { name, group: "release-manifest", value: release(mutate), expectedValid: false, expectedIssueCode }; +} + +/** Structure-only cases for `validateReleaseManifest`; signatures are placeholders. */ +export const RELEASE_MANIFEST_FIXTURES: FixtureCase[] = [ + { name: "valid multi-file release", group: "release-manifest", value: signedReleaseDraft(), expectedValid: true }, + { + name: "valid minimal release", + group: "release-manifest", + value: signedReleaseDraft(RELEASE_MANIFEST_MINIMAL_FIXTURE), + expectedValid: true, + }, + rejected("rejects an unknown top-level field", "unknown_field", (d) => { + d.price = 0; + }), + rejected("rejects an unknown file field", "unknown_field", (d) => { + d.files[0]!.executable = false; + }), + rejected("rejects a missing signature", "required", (d) => { + delete d.signature; + }), + rejected("rejects an uppercase releaseDigest", "pattern", (d) => { + d.releaseDigest = String(d.releaseDigest).toUpperCase().replace("SHA256", "sha256"); + }), + rejected("rejects a bare-hex artifactDigest", "pattern", (d) => { + d.artifactDigest = "c".repeat(64); + }), + rejected("rejects a parent-directory path", "tree.unsafe_path", (d) => { + d.files[0]!.path = "../x"; + }), + rejected("rejects a duplicate path", "duplicate", (d) => { + d.files[1]!.path = "SKILL.md"; + }), + rejected("rejects unsorted files", "order", (d) => { + d.files.reverse(); + }), + rejected("rejects a case-colliding path", "tree.case_collision", (d) => { + d.files[0]!.path = "Scripts/check.sh"; + d.files.sort((a, b) => (String(a.path) < String(b.path) ? -1 : 1)); + }), + rejected("rejects a non-NFC path", "tree.non_nfc_path", (d) => { + // "e" followed by U+0301 COMBINING ACUTE ACCENT, the NFD form of U+00E9. + d.files[1]!.path = `references/cafe${String.fromCodePoint(0x301)}.md`; + }), + rejected("rejects excluded noise", "tree.unsafe_path", (d) => { + d.files[1]!.path = "references/.DS_Store"; + }), + rejected("rejects an unknown file mode", "unsupported_value", (d) => { + d.files[0]!.mode = "120000"; + }), + rejected("rejects an uppercase file hash", "pattern", (d) => { + d.files[0]!.sha256 = String(d.files[0]!.sha256).toUpperCase(); + }), + rejected("rejects a release state outside the enum", "unsupported_value", (d) => { + d.state = "blocked"; + }), + rejected("rejects the reserved @agentkit publisher", "identity.reserved_publisher", (d) => { + d.identity = "@agentkit/review-guardrails"; + d.publisher.handle = "agentkit"; + }), + rejected("rejects an identity with a reserved device-name slug", "identity.invalid", (d) => { + d.identity = "@publisher/con"; + }), + rejected("rejects a publisher handle that differs from the identity", "mismatch", (d) => { + d.publisher.handle = "someone-else"; + }), + rejected("rejects an unknown publisher verification", "unsupported_value", (d) => { + d.publisher.verifiedBy = "self"; + }), + rejected("rejects a wrong fileCount", "mismatch", (d) => { + d.fileCount = 4; + }), + rejected("rejects a wrong treeSize", "mismatch", (d) => { + d.treeSize = 155; + }), + rejected("rejects a declared script missing from files", "not_found", (d) => { + d.declaredScripts = ["scripts/missing.sh"]; + }), + rejected("rejects a non-https source repo", "format", (d) => { + d.sourceProvenance.repo = "http://github.com/publisher/review-guardrails"; + }), + rejected("rejects an abbreviated source commit", "pattern", (d) => { + d.sourceProvenance.commit = "0123456"; + }), + rejected("rejects a timestamp with fractional seconds", "format", (d) => { + d.createdAt = "2026-10-13T04:30:00.000Z"; + }), + rejected("rejects an impossible calendar date", "format", (d) => { + d.createdAt = "2026-02-30T00:00:00Z"; + }), + rejected("rejects a key ID outside the registry pattern", "pattern", (d) => { + d.keyId = "Key ID"; + }), + rejected("rejects another signature algorithm", "unsupported_value", (d) => { + d.signatureAlgorithm = "rsa-pss"; + }), + rejected("rejects an unpadded signature", "pattern", (d) => { + d.signature = "A".repeat(86); + }), +]; diff --git a/packages/contracts/src/fixtures/release-manifest-v1.json b/packages/contracts/src/fixtures/release-manifest-v1.json new file mode 100644 index 0000000..6735ab6 --- /dev/null +++ b/packages/contracts/src/fixtures/release-manifest-v1.json @@ -0,0 +1,1865 @@ +{ + "description": "skillx.release/v1 signed manifest vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", + "canonicalization": "signature = padded base64 Ed25519 over UTF-8 canonical JSON (RFC 8785 subset: members sorted by UTF-16 code units, no whitespace, non-negative safe integers only, only quote/backslash/C0 controls escaped, everything else raw UTF-8) of the manifest without its signature member; see packages/contracts/src/canonical-json.ts", + "testKey": { + "keyId": "skillx-test-vector-1", + "seedText": "skillx.release/v1 test vector key; public, never trusted", + "seedHex": "1ec7ec7c27d6e97182ca8a8c0e2f3f2af27f4f7174f70ba70dbfd54f6c12c575", + "publicKeyHex": "8029f998dfaf0fd73e5bdc7762280fbb0a9805af218308d2df9213c64cdfadb2", + "publicKeyBase64": "gCn5mN+vD9c+W9x3YigPuwqYBa8hgwjS35ITxkzfrbI=" + }, + "valid": [ + { + "name": "published multi-file release", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "X7m1EkEXrg5sIbEcG/d2MPXB4J5o/QsEOYvBMWvzJy0LKAgZN3hqCNZeknJR9XzUdpp4eRtmjPgvalTo+0L3DQ==" + }, + "signingPayload": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"state\":\"published\",\"treeSize\":154,\"version\":\"1.2.0\"}", + "signingPayloadSha256": "2dbd8ec2f29ac3582e0e754efc8b2c3a4766f23f05e173abfd6038ab25242214", + "fileContentsBase64": { + "SKILL.md": "LS0tCm5hbWU6IHJldmlldy1ndWFyZHJhaWxzCmRlc2NyaXB0aW9uOiBSZXZpZXcgZ3VhcmRyYWlscyBmb3IgcHVsbCByZXF1ZXN0cy4KLS0tCiMgUmV2aWV3IGd1YXJkcmFpbHMK", + "references/café & notes
.md": "Tm90ZXMgd2l0aCBhIGxpbmUgc2VwYXJhdG9yIHBhdGguCg==", + "scripts/check.sh": "IyEvYmluL3NoCmVjaG8gb2sK" + } + }, + { + "name": "quarantined minimal prerelease", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@solo.dev/tiny_skill", + "version": "0.1.0-beta.1", + "channel": "beta", + "kind": "skill", + "releaseDigest": "sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544", + "artifactDigest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "artifactSize": 1536, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d", + "size": 44 + } + ], + "fileCount": 1, + "treeSize": 44, + "state": "quarantined", + "publisher": { + "handle": "solo.dev", + "verifiedBy": "none" + }, + "sourceProvenance": null, + "declaredScripts": [], + "createdAt": "2026-10-13T05:00:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "4/jiQH7dCgeJ+ZSR52FVtR9FJXDlOoxAwFT5g6HytAe/lN7NpAJDnzBwLXN5biN2jCaJRhUyIG/2UHWuU6n0Dw==" + }, + "signingPayload": "{\"artifactDigest\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"artifactSize\":1536,\"channel\":\"beta\",\"createdAt\":\"2026-10-13T05:00:00Z\",\"declaredScripts\":[],\"fileCount\":1,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d\",\"size\":44}],\"identity\":\"@solo.dev/tiny_skill\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"solo.dev\",\"verifiedBy\":\"none\"},\"releaseDigest\":\"sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544\",\"schemaVersion\":\"skillx.release/v1\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":null,\"state\":\"quarantined\",\"treeSize\":44,\"version\":\"0.1.0-beta.1\"}", + "signingPayloadSha256": "c26ee8e521c41bf3e116225e79d3b2a5973e77dfbe22c3f4962b63b1263d4fd7", + "fileContentsBase64": { + "SKILL.md": "LS0tCm5hbWU6IHRpbnkKZGVzY3JpcHRpb246IFRpbnkgc2tpbGwuCi0tLQo=" + } + } + ], + "invalidSignature": [ + { + "name": "state changed after signing", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "yanked", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "X7m1EkEXrg5sIbEcG/d2MPXB4J5o/QsEOYvBMWvzJy0LKAgZN3hqCNZeknJR9XzUdpp4eRtmjPgvalTo+0L3DQ==" + }, + "expectedCode": "signature.mismatch" + }, + { + "name": "flipped signature byte", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Xrm1EkEXrg5sIbEcG/d2MPXB4J5o/QsEOYvBMWvzJy0LKAgZN3hqCNZeknJR9XzUdpp4eRtmjPgvalTo+0L3DQ==" + }, + "expectedCode": "signature.mismatch" + }, + { + "name": "signed by another key under the trusted key ID", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "xHoZb1jFtQRzh2cNjc16ptow+NB9zuvO7KCdvqqWrBLWwPVAQ8IoS2D550rCj+Usqe4holIV3alFOYfgIL5sDg==" + }, + "expectedCode": "signature.mismatch" + }, + { + "name": "unknown key ID", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-unknown-key", + "signatureAlgorithm": "ed25519", + "signature": "X7m1EkEXrg5sIbEcG/d2MPXB4J5o/QsEOYvBMWvzJy0LKAgZN3hqCNZeknJR9XzUdpp4eRtmjPgvalTo+0L3DQ==" + }, + "expectedCode": "signature.unknown_key" + }, + { + "name": "signature over an encoding/json-escaped payload", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "0/2aFw3vNJA1qTvsyWiwpI4YQVZTo3H43udjoWG+E+H9zZuBiLCgySXpa/qUn01dqm6tJbS3o4ykXjLQFLfSDQ==" + }, + "expectedCode": "signature.mismatch" + } + ], + "invalidManifest": [ + { + "name": "rejects an unknown top-level field", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "price": 0 + }, + "expectedCode": "unknown_field" + }, + { + "name": "rejects an unknown file field", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102, + "executable": false + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "unknown_field" + }, + { + "name": "rejects a missing signature", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519" + }, + "expectedCode": "required" + }, + { + "name": "rejects an uppercase releaseDigest", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4C2EAC09C7BA5FC5E0D642959C7E38B65A53CA08E65B8E728D6CA5798E458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "pattern" + }, + { + "name": "rejects a bare-hex artifactDigest", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "pattern" + }, + { + "name": "rejects a parent-directory path", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "../x", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "tree.unsafe_path" + }, + { + "name": "rejects a duplicate path", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "duplicate" + }, + { + "name": "rejects unsorted files", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "order" + }, + { + "name": "rejects a case-colliding path", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "Scripts/check.sh", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "tree.case_collision" + }, + { + "name": "rejects a non-NFC path", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "tree.non_nfc_path" + }, + { + "name": "rejects excluded noise", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/.DS_Store", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "tree.unsafe_path" + }, + { + "name": "rejects an unknown file mode", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "120000", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "unsupported_value" + }, + { + "name": "rejects an uppercase file hash", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "BCFDC953C1AF6A7FDFDE9D0A99D73BFBA6A5FAD8BB81D484B3BB416461AB5B11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "pattern" + }, + { + "name": "rejects a release state outside the enum", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "blocked", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "unsupported_value" + }, + { + "name": "rejects the reserved @agentkit publisher", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@agentkit/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "agentkit", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "identity.reserved_publisher" + }, + { + "name": "rejects an identity with a reserved device-name slug", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/con", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "identity.invalid" + }, + { + "name": "rejects a publisher handle that differs from the identity", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "someone-else", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "mismatch" + }, + { + "name": "rejects an unknown publisher verification", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "self" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "unsupported_value" + }, + { + "name": "rejects a wrong fileCount", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 4, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "mismatch" + }, + { + "name": "rejects a wrong treeSize", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 155, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "mismatch" + }, + { + "name": "rejects a declared script missing from files", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/missing.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "not_found" + }, + { + "name": "rejects a non-https source repo", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "http://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "format" + }, + { + "name": "rejects an abbreviated source commit", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "pattern" + }, + { + "name": "rejects a timestamp with fractional seconds", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00.000Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "format" + }, + { + "name": "rejects an impossible calendar date", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-02-30T00:00:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "format" + }, + { + "name": "rejects a key ID outside the registry pattern", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "Key ID", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "pattern" + }, + { + "name": "rejects another signature algorithm", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "rsa-pss", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "unsupported_value" + }, + { + "name": "rejects an unpadded signature", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "state": "published", + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + "expectedCode": "pattern" + } + ] +} diff --git a/packages/contracts/src/fixtures/types.ts b/packages/contracts/src/fixtures/types.ts index e1a95aa..6639821 100644 --- a/packages/contracts/src/fixtures/types.ts +++ b/packages/contracts/src/fixtures/types.ts @@ -20,7 +20,8 @@ export type FixtureGroup = | "collection-member" | "entitlement" | "listing-import" - | "release-publish"; + | "release-publish" + | "release-manifest"; /** A syntactically valid digest used by fixtures. */ export const FIXTURE_DIGEST = `sha256:${"a".repeat(64)}`; diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index b9b02ba..c3d8e48 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -10,6 +10,8 @@ export * from "./semver-range"; export * from "./verification-evidence"; export * from "./compatibility"; export * from "./package-manifest"; +export * from "./canonical-json"; +export * from "./release-manifest"; export * from "./collections"; export * from "./ingestion"; export * from "./dto"; diff --git a/packages/contracts/src/package-manifest.test.ts b/packages/contracts/src/package-manifest.test.ts index 3230661..2d4d93d 100644 --- a/packages/contracts/src/package-manifest.test.ts +++ b/packages/contracts/src/package-manifest.test.ts @@ -6,8 +6,10 @@ import { INGESTION_FIXTURES, PACKAGE_MANIFEST_FIXTURES, PACKAGE_MANIFEST_FIXTURE, + RELEASE_MANIFEST_FIXTURES, VERIFICATION_EVIDENCE_FIXTURES, } from "./fixtures"; +import { validateReleaseManifest } from "./release-manifest"; import { PACKAGE_KINDS, RELEASE_STATES, @@ -28,12 +30,13 @@ const VALIDATOR_BY_GROUP: Record { valid: boolean; i entitlement: validateEntitlement, "listing-import": validateListingImport, "release-publish": validateReleasePublish, + "release-manifest": validateReleaseManifest, }; describe("skillx.package/v1 fixtures", () => { it("covers both a valid and a rejected case for every contract group", () => { const groups = new Set(ALL_FIXTURES.map((fixture) => fixture.group)); - expect(groups.size).toBe(6); + expect(groups.size).toBe(7); for (const group of groups) { const cases = ALL_FIXTURES.filter((fixture) => fixture.group === group); expect(cases.some((c) => c.expectedValid), `${group} needs a valid fixture`).toBe(true); @@ -56,12 +59,14 @@ describe("skillx.package/v1 fixtures", () => { expect(COLLECTION_MEMBER_FIXTURES.length).toBeGreaterThan(0); expect(ENTITLEMENT_FIXTURES.length).toBeGreaterThan(0); expect(INGESTION_FIXTURES.length).toBeGreaterThan(0); + expect(RELEASE_MANIFEST_FIXTURES.length).toBeGreaterThan(0); expect(ALL_FIXTURES.length).toBe( PACKAGE_MANIFEST_FIXTURES.length + VERIFICATION_EVIDENCE_FIXTURES.length + COLLECTION_MEMBER_FIXTURES.length + ENTITLEMENT_FIXTURES.length + - INGESTION_FIXTURES.length, + INGESTION_FIXTURES.length + + RELEASE_MANIFEST_FIXTURES.length, ); }); }); diff --git a/packages/contracts/src/release-manifest.test.ts b/packages/contracts/src/release-manifest.test.ts new file mode 100644 index 0000000..9f3a869 --- /dev/null +++ b/packages/contracts/src/release-manifest.test.ts @@ -0,0 +1,212 @@ +import { describe, expect, it } from "vitest"; +import { + RELEASE_FIXTURE_KEY_ID, + RELEASE_FIXTURE_UNICODE_PATH, + RELEASE_MANIFEST_FIXTURE, + RELEASE_MANIFEST_MINIMAL_FIXTURE, + signedReleaseDraft, +} from "./fixtures"; +import { + RELEASE_SIGNATURE_CODES, + ReleaseManifestError, + compareUtf8, + releaseSigningPayload, + signReleaseManifest, + validateReleaseManifest, + validateUnsignedReleaseManifest, + verifyReleaseManifest, +} from "./release-manifest"; +import type { SkillxReleaseManifest, UnsignedReleaseManifest } from "./release-manifest"; + +const decoder = new TextDecoder(); + +async function generateKey(): Promise<{ privateKey: CryptoKey; publicKey: Uint8Array }> { + const pair = (await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"])) as CryptoKeyPair; + const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey)); + return { privateKey: pair.privateKey, publicKey }; +} + +/** Rebuilds `value` with every object's keys inserted in reverse order. */ +function reverseKeys(value: unknown): unknown { + if (Array.isArray(value)) return value.map(reverseKeys); + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .reverse() + .map(([key, item]) => [key, reverseKeys(item)]), + ); + } + return value; +} + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function flipBase64Byte(signature: string): string { + const bytes = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)); + bytes[0] = bytes[0]! ^ 0x01; + return btoa(String.fromCharCode(...bytes)); +} + +describe("skillx.release/v1 validation", () => { + it("accepts both unsigned fixtures before signing", () => { + expect(validateUnsignedReleaseManifest(RELEASE_MANIFEST_FIXTURE)).toEqual({ valid: true, issues: [] }); + expect(validateUnsignedReleaseManifest(RELEASE_MANIFEST_MINIMAL_FIXTURE)).toEqual({ valid: true, issues: [] }); + }); + + it("rejects a signature on an unsigned manifest", () => { + const result = validateUnsignedReleaseManifest(signedReleaseDraft()); + expect(result.issues).toContainEqual(expect.objectContaining({ path: "signature", code: "unknown_field" })); + }); + + it("rejects non-object input without throwing", () => { + for (const input of [null, undefined, "skillx.release/v1", [], 7]) { + expect(validateReleaseManifest(input).valid).toBe(false); + } + }); + + it("reports every missing field of an empty object", () => { + const paths = validateReleaseManifest({}).issues.filter((i) => i.code === "required").map((i) => i.path); + expect(paths).toContain("releaseDigest"); + expect(paths).toContain("signature"); + expect(paths).toHaveLength(19); + }); + + it("rejects unknown nested publisher and provenance fields", () => { + const draft = signedReleaseDraft() as Record>; + draft.publisher!.email = "x@example.com"; + draft.sourceProvenance!.ref = "main"; + const paths = validateReleaseManifest(draft).issues.map((i) => `${i.path}:${i.code}`); + expect(paths).toContain("publisher.email:unknown_field"); + expect(paths).toContain("sourceProvenance.ref:unknown_field"); + }); + + it("accepts a release at the repository root", () => { + const draft = signedReleaseDraft() as Record>; + draft.sourceProvenance!.path = ""; + expect(validateReleaseManifest(draft).valid).toBe(true); + }); + + it("rejects unsorted declared scripts", () => { + const draft = signedReleaseDraft(); + draft.declaredScripts = ["scripts/check.sh", "SKILL.md"]; + expect(validateReleaseManifest(draft).issues.map((i) => i.code)).toContain("order"); + }); + + it("orders paths by UTF-8 bytes, not UTF-16 code units", () => { + // U+FF61 is one UTF-16 unit above the surrogates but sorts before U+1F600 in UTF-8. + expect(compareUtf8(String.fromCodePoint(0xff61), String.fromCodePoint(0x1f600))).toBe(-1); + expect(compareUtf8("a", "a/")).toBe(-1); + expect(compareUtf8("b", "b")).toBe(0); + }); +}); + +describe("skillx.release/v1 signing payload", () => { + it("is byte-stable across member order", async () => { + const reordered = reverseKeys(RELEASE_MANIFEST_FIXTURE) as UnsignedReleaseManifest; + expect(Object.keys(reordered)[0]).toBe("signatureAlgorithm"); + expect(releaseSigningPayload(reordered)).toEqual(releaseSigningPayload(RELEASE_MANIFEST_FIXTURE)); + }); + + it("excludes only the signature member", async () => { + const { privateKey } = await generateKey(); + const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); + expect(releaseSigningPayload(signed)).toEqual(releaseSigningPayload(RELEASE_MANIFEST_FIXTURE)); + const text = decoder.decode(releaseSigningPayload(signed)); + expect(text).not.toContain('"signature"'); + expect(text).toContain('"keyId":"skillx-test-vector-1"'); + expect(text).toContain('"signatureAlgorithm":"ed25519"'); + expect(text.startsWith('{"artifactDigest":')).toBe(true); + expect(text).toContain('"sourceProvenance":{"commit":'); + expect(text).toContain(RELEASE_FIXTURE_UNICODE_PATH); + expect(text).not.toContain("\\u2028"); + }); + + it("encodes null provenance and empty script lists literally", () => { + const text = decoder.decode(releaseSigningPayload(RELEASE_MANIFEST_MINIMAL_FIXTURE)); + expect(text).toContain('"declaredScripts":[]'); + expect(text).toContain('"sourceProvenance":null'); + }); +}); + +describe("skillx.release/v1 signatures", () => { + it("verifies a manifest signed with the trusted key", async () => { + const { privateKey, publicKey } = await generateKey(); + const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); + const keys = new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]]); + expect(await verifyReleaseManifest(signed, keys)).toEqual({ valid: true, issues: [] }); + expect(await verifyReleaseManifest(reverseKeys(signed), keys)).toEqual({ valid: true, issues: [] }); + }); + + it.each<[string, (m: SkillxReleaseManifest) => void]>([ + ["a changed state", (m) => void (m.state = "yanked")], + ["a changed file hash", (m) => void (m.files[0]!.sha256 = `${m.files[0]!.sha256.slice(0, 63)}0`)], + ["a changed release digest", (m) => void (m.releaseDigest = `sha256:${"e".repeat(64)}`)], + ["a changed publisher verification", (m) => void (m.publisher.verifiedBy = "admin")], + ["a flipped signature byte", (m) => void (m.signature = flipBase64Byte(m.signature))], + ])("rejects %s", async (_name, tamper) => { + const { privateKey, publicKey } = await generateKey(); + const signed = clone(await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey)); + tamper(signed); + expect(validateReleaseManifest(signed).valid).toBe(true); + const result = await verifyReleaseManifest(signed, new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]])); + expect(result.valid).toBe(false); + expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + }); + + it("rejects a signature from another key under the trusted key ID", async () => { + const signer = await generateKey(); + const trusted = await generateKey(); + const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, signer.privateKey); + const result = await verifyReleaseManifest(signed, new Map([[RELEASE_FIXTURE_KEY_ID, trusted.publicKey]])); + expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + }); + + it("rejects a key ID swapped to another trusted key, because the key ID is signed", async () => { + const a = await generateKey(); + const b = await generateKey(); + const signed = clone(await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, a.privateKey)); + signed.keyId = "skillx-other-key"; + const keys = new Map([ + [RELEASE_FIXTURE_KEY_ID, a.publicKey], + ["skillx-other-key", a.publicKey], + ["skillx-third-key", b.publicKey], + ]); + const result = await verifyReleaseManifest(signed, keys); + expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + }); + + it("rejects an unknown key ID", async () => { + const { privateKey, publicKey } = await generateKey(); + const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); + const result = await verifyReleaseManifest(signed, new Map([["skillx-other-key", publicKey]])); + expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.unknownKey]); + }); + + it("rejects a malformed public key without throwing", async () => { + const { privateKey } = await generateKey(); + const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); + const result = await verifyReleaseManifest(signed, new Map([[RELEASE_FIXTURE_KEY_ID, new Uint8Array(31)]])); + expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.invalidKey]); + }); + + it("returns structural issues for an invalid manifest instead of verifying", async () => { + const { publicKey } = await generateKey(); + const result = await verifyReleaseManifest({ schemaVersion: "skillx.release/v1" }, new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]])); + expect(result.valid).toBe(false); + expect(result.issues.map((i) => i.code)).not.toContain(RELEASE_SIGNATURE_CODES.mismatch); + }); + + it("refuses to sign an invalid manifest", async () => { + const { privateKey } = await generateKey(); + const invalid = { ...RELEASE_MANIFEST_FIXTURE, state: "blocked" } as unknown as UnsignedReleaseManifest; + await expect(signReleaseManifest(invalid, privateKey)).rejects.toBeInstanceOf(ReleaseManifestError); + }); + + it("refuses to sign with a verify-only key", async () => { + const { publicKey } = await generateKey(); + const verifyKey = await crypto.subtle.importKey("raw", new Uint8Array(publicKey), { name: "Ed25519" }, false, ["verify"]); + await expect(signReleaseManifest(RELEASE_MANIFEST_FIXTURE, verifyKey)).rejects.toBeInstanceOf(ReleaseManifestError); + }); +}); diff --git a/packages/contracts/src/release-manifest.ts b/packages/contracts/src/release-manifest.ts new file mode 100644 index 0000000..abd3203 --- /dev/null +++ b/packages/contracts/src/release-manifest.ts @@ -0,0 +1,490 @@ +/** + * `skillx.release/v1` signed release manifest contract. + * + * A release manifest pins one immutable package version to the canonical + * `agentkit.skill-tree/v1` tree of its files. `releaseDigest` is the tree + * digest and the release identity; `artifactDigest` covers only the bytes of + * the transport archive. The registry signs the manifest with Ed25519 over + * `releaseSigningPayload`: the canonical JSON (see `canonical-json.ts`) of + * every member except `signature`, so `keyId` and `signatureAlgorithm` are + * signed too. The signature is the standard (padded) base64 of the 64-byte + * Ed25519 signature, as in AgentKit's remote registry manifests. + * + * Validators never throw on malformed input. Signing and verification use the + * WebCrypto `Ed25519` algorithm, available in Workers, browsers and Node.js. + */ + +import { ERROR_CODES, SkillTreeError, checkCollisions, checkPublisher, isNoisePath, validatePath } from "@skillx/skill-tree/rules"; +import { canonicalJsonBytes } from "./canonical-json"; +import { + PACKAGE_NAME_PATTERN, + RELEASE_CHANNELS, + SEMVER_PATTERN, + isPackageKind, + isReleaseState, + PACKAGE_KINDS, + RELEASE_STATES, +} from "./package-manifest"; +import type { PackageKind, ReleaseChannel, ReleaseState } from "./package-manifest"; +import { isPlainObject, issue, validationFail, validationOk } from "./validation"; +import type { ValidationIssue, ValidationResult } from "./validation"; +import { RELEASE_DIGEST_PATTERN } from "./verification-evidence"; + +export const SKILLX_RELEASE_SCHEMA_VERSION = "skillx.release/v1"; +export const RELEASE_SIGNATURE_ALGORITHM = "ed25519"; + +/** Mode classes of `agentkit.skill-tree/v1`. */ +export const RELEASE_FILE_MODES = ["100644", "100755"] as const; +export type ReleaseFileMode = (typeof RELEASE_FILE_MODES)[number]; + +/** + * How SkillX verified that the publisher owns its handle. `none` means the + * release is not publisher-verified, so AgentKit trusts it as `local-unverified`. + */ +export const PUBLISHER_VERIFICATIONS = ["none", "github", "admin"] as const; +export type PublisherVerification = (typeof PUBLISHER_VERIFICATIONS)[number]; + +/** Same key ID rule as AgentKit remote registry manifests. */ +export const RELEASE_KEY_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{1,63}$/; +/** Per-file content hash: bare lowercase hex SHA-256. */ +export const FILE_SHA256_PATTERN = /^[a-f0-9]{64}$/; +/** A full SHA-1 or SHA-256 git object name in lowercase hex. */ +export const SOURCE_COMMIT_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/; +/** RFC 3339 UTC timestamp with whole seconds, e.g. `2026-10-13T04:30:00Z`. */ +export const RELEASE_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; +/** Padded standard base64 of a 64-byte Ed25519 signature. */ +export const RELEASE_SIGNATURE_PATTERN = /^[A-Za-z0-9+/]{86}==$/; +const MAX_SOURCE_REPO_LENGTH = 512; + +/** Tree limits of `agentkit.skill-tree/v1`; equal to `LIMITS` of `@skillx/skill-tree` (guarded by a test). */ +export const RELEASE_TREE_LIMITS = { + maxFiles: 4_096, + maxFileSize: 32 * 1024 * 1024, + maxTreeSize: 256 * 1024 * 1024, +} as const; + +export interface ReleaseFile { + path: string; + mode: ReleaseFileMode; + /** Lowercase hex SHA-256 of the file content. */ + sha256: string; + size: number; +} + +export interface ReleasePublisher { + /** The publisher segment of `identity`. */ + handle: string; + verifiedBy: PublisherVerification; +} + +export interface ReleaseSourceProvenance { + /** HTTPS URL of the source repository. */ + repo: string; + commit: string; + /** Package root inside the repository; `""` for the repository root. */ + path: string; +} + +export interface UnsignedReleaseManifest { + schemaVersion: typeof SKILLX_RELEASE_SCHEMA_VERSION; + identity: string; + version: string; + channel: ReleaseChannel; + kind: PackageKind; + /** `treeDigest` of `files`: `sha256:<64 lowercase hex>`. */ + releaseDigest: string; + /** Transport archive digest; never an identity. */ + artifactDigest: string; + artifactSize: number; + /** Sorted by the raw UTF-8 bytes of `path`. */ + files: ReleaseFile[]; + fileCount: number; + treeSize: number; + /** Release state at signing time; a state change is a newly signed manifest for the same digest. */ + state: ReleaseState; + publisher: ReleasePublisher; + /** `null` when the release was not packed from a recorded git commit. */ + sourceProvenance: ReleaseSourceProvenance | null; + /** Paths of `files` the package declares as scripts, sorted like `files`. Never executed by SkillX. */ + declaredScripts: string[]; + createdAt: string; + keyId: string; + signatureAlgorithm: typeof RELEASE_SIGNATURE_ALGORITHM; +} + +export interface SkillxReleaseManifest extends UnsignedReleaseManifest { + signature: string; +} + +const MANIFEST_FIELDS = [ + "schemaVersion", + "identity", + "version", + "channel", + "kind", + "releaseDigest", + "artifactDigest", + "artifactSize", + "files", + "fileCount", + "treeSize", + "state", + "publisher", + "sourceProvenance", + "declaredScripts", + "createdAt", + "keyId", + "signatureAlgorithm", +] as const; +const FILE_FIELDS = ["path", "mode", "sha256", "size"] as const; +const PUBLISHER_FIELDS = ["handle", "verifiedBy"] as const; +const PROVENANCE_FIELDS = ["repo", "commit", "path"] as const; + +/** Signature verification failure codes. */ +export const RELEASE_SIGNATURE_CODES = { + unknownKey: "signature.unknown_key", + invalidKey: "signature.invalid_key", + mismatch: "signature.mismatch", +} as const; + +/** Thrown by `signReleaseManifest` for an invalid manifest or key. */ +export class ReleaseManifestError extends Error { + readonly issues: ValidationIssue[]; + constructor(message: string, issues: ValidationIssue[] = []) { + super(issues.length === 0 ? message : `${message}: ${issues.map((i) => `${i.path} ${i.code}`).join(", ")}`); + this.name = "ReleaseManifestError"; + this.issues = issues; + } +} + +/** Compares well-formed strings by UTF-8 bytes, which is code point order. */ +export function compareUtf8(a: string, b: string): number { + const ia = a[Symbol.iterator](); + const ib = b[Symbol.iterator](); + for (;;) { + const ca = ia.next(); + const cb = ib.next(); + if (ca.done || cb.done) return ca.done && cb.done ? 0 : ca.done ? -1 : 1; + const diff = ca.value.codePointAt(0)! - cb.value.codePointAt(0)!; + if (diff !== 0) return diff < 0 ? -1 : 1; + } +} + +const isSize = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0; + +function oneOf(values: readonly T[], value: unknown): value is T { + return typeof value === "string" && (values as readonly string[]).includes(value); +} + +function checkFields(value: Record, allowed: readonly string[], path: string, issues: ValidationIssue[]): void { + const prefix = path === "" ? "" : `${path}.`; + for (const key of Object.keys(value)) { + if (!allowed.includes(key)) issues.push(issue(`${prefix}${key}`, "unknown_field", "is not a skillx.release/v1 field")); + } + for (const key of allowed) { + if (!(key in value)) issues.push(issue(`${prefix}${key}`, "required", "is required")); + } +} + +function treeIssue(path: string, error: SkillTreeError): ValidationIssue { + return issue(path, error.code, error.message); +} + +function validateIdentityField(identity: unknown, issues: ValidationIssue[]): string | null { + if (typeof identity !== "string" || !PACKAGE_NAME_PATTERN.test(identity)) { + issues.push(issue("identity", "pattern", "must be '@publisher/slug' using lowercase slug characters")); + return null; + } + try { + // Every SkillX release comes from the `skillx` provider, so `@agentkit/*` is always reserved. + checkPublisher(identity, "skillx"); + } catch (error) { + if (!(error instanceof SkillTreeError)) throw error; + issues.push(treeIssue("identity", error)); + return null; + } + return identity.slice(1, identity.indexOf("/")); +} + +function validateFiles(files: unknown, issues: ValidationIssue[]): ReleaseFile[] | null { + if (!Array.isArray(files)) { + issues.push(issue("files", "type", "must be an array")); + return null; + } + if (files.length === 0) issues.push(issue("files", ERROR_CODES.empty, "a release has at least one file")); + if (files.length > RELEASE_TREE_LIMITS.maxFiles) { + issues.push(issue("files", ERROR_CODES.limitExceeded, `has more than ${RELEASE_TREE_LIMITS.maxFiles} files`)); + return null; + } + const before = issues.length; + let previous: string | null = null; + files.forEach((file: unknown, i) => { + const at = `files[${i}]`; + if (!isPlainObject(file)) { + issues.push(issue(at, "type", "must be an object")); + return; + } + checkFields(file, FILE_FIELDS, at, issues); + const { path, mode, sha256, size } = file; + if (typeof path !== "string") { + issues.push(issue(`${at}.path`, "type", "must be a string")); + } else { + const pathError = validatePath(path); + if (pathError !== null) issues.push(treeIssue(`${at}.path`, pathError)); + else if (isNoisePath(path)) issues.push(issue(`${at}.path`, ERROR_CODES.unsafePath, "is excluded noise that no tree contains")); + if (previous !== null) { + const order = compareUtf8(previous, path); + if (order === 0) issues.push(issue(`${at}.path`, "duplicate", `duplicates ${JSON.stringify(path)}`)); + else if (order > 0) issues.push(issue(`${at}.path`, "order", "files must be sorted by raw UTF-8 path bytes")); + } + previous = path; + } + if (!oneOf(RELEASE_FILE_MODES, mode)) { + issues.push(issue(`${at}.mode`, "unsupported_value", `must be one of: ${RELEASE_FILE_MODES.join(", ")}`)); + } + if (typeof sha256 !== "string" || !FILE_SHA256_PATTERN.test(sha256)) { + issues.push(issue(`${at}.sha256`, "pattern", "must be 64 lowercase hex characters")); + } + if (!isSize(size)) issues.push(issue(`${at}.size`, "type", "must be a non-negative safe integer")); + else if (size > RELEASE_TREE_LIMITS.maxFileSize) { + issues.push(issue(`${at}.size`, ERROR_CODES.limitExceeded, `exceeds ${RELEASE_TREE_LIMITS.maxFileSize} bytes`)); + } + }); + if (issues.length !== before) return null; + const valid = files as ReleaseFile[]; + const collision = checkCollisions(valid.map((f) => f.path), []); + if (collision !== null) { + issues.push(treeIssue("files", collision)); + return null; + } + return valid; +} + +function validatePublisher(publisher: unknown, handle: string | null, issues: ValidationIssue[]): void { + if (!isPlainObject(publisher)) { + issues.push(issue("publisher", "type", "must be an object")); + return; + } + checkFields(publisher, PUBLISHER_FIELDS, "publisher", issues); + if (typeof publisher.handle !== "string") { + issues.push(issue("publisher.handle", "type", "must be a string")); + } else if (handle !== null && publisher.handle !== handle) { + issues.push(issue("publisher.handle", "mismatch", `must equal the identity publisher ${JSON.stringify(handle)}`)); + } + if (!oneOf(PUBLISHER_VERIFICATIONS, publisher.verifiedBy)) { + issues.push(issue("publisher.verifiedBy", "unsupported_value", `must be one of: ${PUBLISHER_VERIFICATIONS.join(", ")}`)); + } +} + +function isHttpsRepo(value: unknown): boolean { + if (typeof value !== "string" || value.length > MAX_SOURCE_REPO_LENGTH || !/^https:\/\/[\x21-\x7e]+$/.test(value)) return false; + try { + const url = new URL(value); + return url.protocol === "https:" && url.username === "" && url.password === "" && url.hostname !== ""; + } catch { + return false; + } +} + +function validateProvenance(provenance: unknown, issues: ValidationIssue[]): void { + if (provenance === null) return; + if (!isPlainObject(provenance)) { + issues.push(issue("sourceProvenance", "type", "must be an object or null")); + return; + } + checkFields(provenance, PROVENANCE_FIELDS, "sourceProvenance", issues); + if (!isHttpsRepo(provenance.repo)) { + issues.push(issue("sourceProvenance.repo", "format", `must be an https URL without credentials, at most ${MAX_SOURCE_REPO_LENGTH} characters`)); + } + if (typeof provenance.commit !== "string" || !SOURCE_COMMIT_PATTERN.test(provenance.commit)) { + issues.push(issue("sourceProvenance.commit", "pattern", "must be a full lowercase hex git commit")); + } + if (typeof provenance.path !== "string") { + issues.push(issue("sourceProvenance.path", "type", "must be a string")); + } else if (provenance.path !== "") { + const pathError = validatePath(provenance.path); + if (pathError !== null) issues.push(treeIssue("sourceProvenance.path", pathError)); + } +} + +function validateDeclaredScripts(scripts: unknown, files: ReleaseFile[] | null, issues: ValidationIssue[]): void { + if (!Array.isArray(scripts)) { + issues.push(issue("declaredScripts", "type", "must be an array of file paths")); + return; + } + const paths = files === null ? null : new Set(files.map((f) => f.path)); + let previous: string | null = null; + scripts.forEach((script: unknown, i) => { + const at = `declaredScripts[${i}]`; + if (typeof script !== "string") { + issues.push(issue(at, "type", "must be a string")); + return; + } + if (paths !== null && !paths.has(script)) issues.push(issue(at, "not_found", "must name a path in files")); + if (previous !== null && compareUtf8(previous, script) >= 0) { + issues.push(issue(at, "order", "must be unique and sorted by raw UTF-8 path bytes")); + } + previous = script; + }); +} + +function isReleaseTimestamp(value: unknown): boolean { + if (typeof value !== "string" || !RELEASE_TIMESTAMP_PATTERN.test(value)) return false; + const ms = Date.parse(value); + // Round-tripping rejects calendar overflow such as 2026-02-30. + return Number.isFinite(ms) && new Date(ms).toISOString() === `${value.slice(0, -1)}.000Z`; +} + +function collectIssues(input: unknown, requireSignature: boolean): ValidationIssue[] { + if (!isPlainObject(input)) return [issue("", "type", "release manifest must be an object")]; + const issues: ValidationIssue[] = []; + checkFields(input, requireSignature ? [...MANIFEST_FIELDS, "signature"] : MANIFEST_FIELDS, "", issues); + + if (input.schemaVersion !== SKILLX_RELEASE_SCHEMA_VERSION) { + issues.push(issue("schemaVersion", "unsupported_value", `must equal '${SKILLX_RELEASE_SCHEMA_VERSION}'`)); + } + const handle = validateIdentityField(input.identity, issues); + if (typeof input.version !== "string" || !SEMVER_PATTERN.test(input.version)) { + issues.push(issue("version", "pattern", "must be a semantic version such as '0.1.0'")); + } + if (!oneOf(RELEASE_CHANNELS, input.channel)) { + issues.push(issue("channel", "unsupported_value", `must be one of: ${RELEASE_CHANNELS.join(", ")}`)); + } + if (!isPackageKind(input.kind)) { + issues.push(issue("kind", "unsupported_value", `must be one of: ${PACKAGE_KINDS.join(", ")}`)); + } + for (const field of ["releaseDigest", "artifactDigest"] as const) { + const value = input[field]; + if (typeof value !== "string" || !RELEASE_DIGEST_PATTERN.test(value)) { + issues.push(issue(field, "pattern", "must match sha256:<64 lowercase hex>")); + } + } + if (!isSize(input.artifactSize) || input.artifactSize === 0) { + issues.push(issue("artifactSize", "type", "must be a positive safe integer")); + } + + const files = validateFiles(input.files, issues); + if (!isSize(input.fileCount)) { + issues.push(issue("fileCount", "type", "must be a non-negative safe integer")); + } else if (files !== null && input.fileCount !== files.length) { + issues.push(issue("fileCount", "mismatch", "must equal the number of files")); + } + if (!isSize(input.treeSize)) { + issues.push(issue("treeSize", "type", "must be a non-negative safe integer")); + } else if (input.treeSize > RELEASE_TREE_LIMITS.maxTreeSize) { + issues.push(issue("treeSize", ERROR_CODES.limitExceeded, `exceeds ${RELEASE_TREE_LIMITS.maxTreeSize} bytes`)); + } else if (files !== null && input.treeSize !== files.reduce((sum, f) => sum + f.size, 0)) { + issues.push(issue("treeSize", "mismatch", "must equal the sum of file sizes")); + } + + if (!isReleaseState(input.state)) { + issues.push(issue("state", "unsupported_value", `must be one of: ${RELEASE_STATES.join(", ")}`)); + } + validatePublisher(input.publisher, handle, issues); + validateProvenance(input.sourceProvenance, issues); + validateDeclaredScripts(input.declaredScripts, files, issues); + if (!isReleaseTimestamp(input.createdAt)) { + issues.push(issue("createdAt", "format", "must be an RFC 3339 UTC timestamp with whole seconds, e.g. 2026-10-13T04:30:00Z")); + } + if (typeof input.keyId !== "string" || !RELEASE_KEY_ID_PATTERN.test(input.keyId)) { + issues.push(issue("keyId", "pattern", `must match ${RELEASE_KEY_ID_PATTERN.source}`)); + } + if (input.signatureAlgorithm !== RELEASE_SIGNATURE_ALGORITHM) { + issues.push(issue("signatureAlgorithm", "unsupported_value", `must equal '${RELEASE_SIGNATURE_ALGORITHM}'`)); + } + if (requireSignature && "signature" in input) { + if (typeof input.signature !== "string" || !RELEASE_SIGNATURE_PATTERN.test(input.signature)) { + issues.push(issue("signature", "pattern", "must be padded base64 of a 64-byte Ed25519 signature")); + } + } + return issues; +} + +/** Validates a signed `skillx.release/v1` manifest (structure only, not the signature). */ +export function validateReleaseManifest(input: unknown): ValidationResult { + const issues = collectIssues(input, true); + return issues.length === 0 ? validationOk() : validationFail(issues); +} + +/** Validates a manifest before it is signed: every field except `signature`, which must be absent. */ +export function validateUnsignedReleaseManifest(input: unknown): ValidationResult { + const issues = collectIssues(input, false); + return issues.length === 0 ? validationOk() : validationFail(issues); +} + +/** + * The exact bytes an Ed25519 release signature covers: the canonical JSON of + * the manifest without its `signature` member, UTF-8 encoded. + */ +export function releaseSigningPayload(manifest: UnsignedReleaseManifest | SkillxReleaseManifest): Uint8Array { + const { signature: _signature, ...payload } = manifest as Partial; + return canonicalJsonBytes(payload); +} + +function toBase64(bytes: Uint8Array): string { + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary); +} + +function fromBase64(text: string): Uint8Array { + const binary = atob(text); + const out = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i); + return out; +} + +const ED25519 = { name: "Ed25519" } as const; +const ED25519_PUBLIC_KEY_BYTES = 32; + +/** + * Signs a valid unsigned manifest with an Ed25519 private key. The key ID and + * algorithm are part of the signed payload. Throws `ReleaseManifestError` when + * the manifest is invalid or the key cannot sign with Ed25519. + */ +export async function signReleaseManifest( + manifest: UnsignedReleaseManifest, + privateKey: CryptoKey, +): Promise { + const result = validateUnsignedReleaseManifest(manifest); + if (!result.valid) throw new ReleaseManifestError("invalid release manifest", result.issues); + let signature: ArrayBuffer; + try { + signature = await crypto.subtle.sign(ED25519, privateKey, releaseSigningPayload(manifest)); + } catch (cause) { + throw new ReleaseManifestError(`cannot sign with the given key: ${cause instanceof Error ? cause.message : String(cause)}`); + } + return { ...manifest, signature: toBase64(new Uint8Array(signature)) }; +} + +/** A key set: key ID to raw 32-byte Ed25519 public key. */ +export type ReleasePublicKeys = ReadonlyMap; + +/** + * Validates a signed manifest and verifies its signature against `publicKeys`. + * Fails closed: an invalid manifest, unknown key ID, malformed key or bad + * signature is an invalid result, never an exception. + */ +export async function verifyReleaseManifest(input: unknown, publicKeys: ReleasePublicKeys): Promise { + const structure = validateReleaseManifest(input); + if (!structure.valid) return structure; + const manifest = input as SkillxReleaseManifest; + const raw = publicKeys.get(manifest.keyId); + if (raw === undefined) { + return validationFail([issue("keyId", RELEASE_SIGNATURE_CODES.unknownKey, `no trusted key ${JSON.stringify(manifest.keyId)}`)]); + } + if (raw.length !== ED25519_PUBLIC_KEY_BYTES) { + return validationFail([issue("keyId", RELEASE_SIGNATURE_CODES.invalidKey, `key ${JSON.stringify(manifest.keyId)} is not 32 bytes`)]); + } + let verified = false; + try { + const key = await crypto.subtle.importKey("raw", new Uint8Array(raw), ED25519, false, ["verify"]); + verified = await crypto.subtle.verify(ED25519, key, fromBase64(manifest.signature), releaseSigningPayload(manifest)); + } catch (cause) { + const detail = cause instanceof Error ? cause.message : String(cause); + return validationFail([issue("keyId", RELEASE_SIGNATURE_CODES.invalidKey, `key ${JSON.stringify(manifest.keyId)} is unusable: ${detail}`)]); + } + return verified + ? validationOk() + : validationFail([issue("signature", RELEASE_SIGNATURE_CODES.mismatch, "signature does not verify for the canonical payload")]); +} diff --git a/packages/contracts/src/release-vectors.test.ts b/packages/contracts/src/release-vectors.test.ts new file mode 100644 index 0000000..b5eea46 --- /dev/null +++ b/packages/contracts/src/release-vectors.test.ts @@ -0,0 +1,204 @@ +import { describe, expect, it } from "vitest"; +import { + RELEASE_FIXTURE_CONTENTS, + RELEASE_FIXTURE_KEY_ID, + RELEASE_MANIFEST_FIXTURE, + RELEASE_MANIFEST_FIXTURES, + RELEASE_MANIFEST_MINIMAL_FIXTURE, +} from "./fixtures"; +import vectorFile from "./fixtures/release-manifest-v1.json"; +import { + RELEASE_SIGNATURE_CODES, + releaseSigningPayload, + signReleaseManifest, + validateReleaseManifest, + verifyReleaseManifest, +} from "./release-manifest"; +import type { SkillxReleaseManifest, UnsignedReleaseManifest } from "./release-manifest"; + +/** + * Golden cross-language vectors for `skillx.release/v1`, exported for + * AgentKit (`fixtures/release-manifest-v1.json`). The signing key is derived + * from a public seed text, so the vectors are reproducible and the key is + * worthless outside tests. + * + * Regenerate after an intentional contract change with + * `SKILLX_WRITE_RELEASE_VECTORS=1 pnpm vitest run packages/contracts/src/release-vectors.test.ts`. + */ + +const KEY_SEED_TEXT = "skillx.release/v1 test vector key; public, never trusted"; +const OTHER_KEY_SEED_TEXT = "skillx.release/v1 other test vector key; public, never trusted"; +const PKCS8_ED25519_PREFIX = [0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20]; + +interface VectorKey { + privateKey: CryptoKey; + seed: Uint8Array; + publicKey: Uint8Array; +} + +interface ReleaseVectors { + description: string; + canonicalization: string; + testKey: { keyId: string; seedText: string; seedHex: string; publicKeyHex: string; publicKeyBase64: string }; + valid: Array<{ + name: string; + manifest: SkillxReleaseManifest; + signingPayload: string; + signingPayloadSha256: string; + fileContentsBase64: Record; + }>; + invalidSignature: Array<{ name: string; manifest: SkillxReleaseManifest; expectedCode: string }>; + invalidManifest: Array<{ name: string; manifest: unknown; expectedCode: string }>; +} + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const hex = (bytes: Uint8Array) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); +const base64 = (bytes: Uint8Array) => btoa(String.fromCharCode(...bytes)); +const fromBase64 = (text: string) => Uint8Array.from(atob(text), (c) => c.charCodeAt(0)); +const fromBase64Url = (text: string) => fromBase64(text.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text.length / 4) * 4, "=")); + +async function sha256(data: Uint8Array): Promise { + return new Uint8Array(await crypto.subtle.digest("SHA-256", data)); +} + +async function vectorKey(seedText: string): Promise { + const seed = await sha256(encoder.encode(seedText)); + const pkcs8 = new Uint8Array([...PKCS8_ED25519_PREFIX, ...seed]); + const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, true, ["sign"]); + const jwk = await crypto.subtle.exportKey("jwk", privateKey); + return { privateKey, seed, publicKey: fromBase64Url(jwk.x ?? "") }; +} + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function contentsBase64(name: keyof typeof RELEASE_FIXTURE_CONTENTS): Record { + return Object.fromEntries(Object.entries(RELEASE_FIXTURE_CONTENTS[name]).map(([path, text]) => [path, base64(encoder.encode(text))])); +} + +/** The payload a Go implementation would get from encoding/json.Marshal-style escaping. */ +function goJsonStylePayload(manifest: UnsignedReleaseManifest): Uint8Array { + const text = decoder.decode(releaseSigningPayload(manifest)).replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028"); + return encoder.encode(text); +} + +async function buildVectors(): Promise { + const key = await vectorKey(KEY_SEED_TEXT); + const other = await vectorKey(OTHER_KEY_SEED_TEXT); + + const valid: ReleaseVectors["valid"] = []; + const signedFull = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, key.privateKey); + for (const [name, base, tree] of [ + ["published multi-file release", RELEASE_MANIFEST_FIXTURE, "full"], + ["quarantined minimal prerelease", RELEASE_MANIFEST_MINIMAL_FIXTURE, "minimal"], + ] as const) { + const manifest = await signReleaseManifest(base, key.privateKey); + const payload = releaseSigningPayload(manifest); + valid.push({ + name, + manifest, + signingPayload: decoder.decode(payload), + signingPayloadSha256: hex(await sha256(payload)), + fileContentsBase64: contentsBase64(tree), + }); + } + + const mismatch = RELEASE_SIGNATURE_CODES.mismatch; + const stateChanged = clone(signedFull); + stateChanged.state = "yanked"; + const flipped = clone(signedFull); + const signatureBytes = fromBase64(flipped.signature); + signatureBytes[0] = signatureBytes[0]! ^ 0x01; + flipped.signature = base64(signatureBytes); + const otherKey = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, other.privateKey); + const unknownKey = clone(signedFull); + unknownKey.keyId = "skillx-unknown-key"; + const goStyle = clone(signedFull); + goStyle.signature = base64( + new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key.privateKey, goJsonStylePayload(RELEASE_MANIFEST_FIXTURE))), + ); + + return { + description: + "skillx.release/v1 signed manifest vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", + canonicalization: + "signature = padded base64 Ed25519 over UTF-8 canonical JSON (RFC 8785 subset: members sorted by UTF-16 code units, no whitespace, non-negative safe integers only, only quote/backslash/C0 controls escaped, everything else raw UTF-8) of the manifest without its signature member; see packages/contracts/src/canonical-json.ts", + testKey: { + keyId: RELEASE_FIXTURE_KEY_ID, + seedText: KEY_SEED_TEXT, + seedHex: hex(key.seed), + publicKeyHex: hex(key.publicKey), + publicKeyBase64: base64(key.publicKey), + }, + valid, + invalidSignature: [ + { name: "state changed after signing", manifest: stateChanged, expectedCode: mismatch }, + { name: "flipped signature byte", manifest: flipped, expectedCode: mismatch }, + { name: "signed by another key under the trusted key ID", manifest: otherKey, expectedCode: mismatch }, + { name: "unknown key ID", manifest: unknownKey, expectedCode: RELEASE_SIGNATURE_CODES.unknownKey }, + { name: "signature over an encoding/json-escaped payload", manifest: goStyle, expectedCode: mismatch }, + ], + invalidManifest: RELEASE_MANIFEST_FIXTURES.filter((c) => !c.expectedValid).map((c) => ({ + name: c.name, + manifest: c.value, + expectedCode: c.expectedIssueCode ?? "", + })), + }; +} + +const committed = vectorFile as unknown as ReleaseVectors; +const writeRequested = (globalThis as { process?: { env: Record } }).process?.env + .SKILLX_WRITE_RELEASE_VECTORS === "1"; + +describe("skillx.release/v1 cross-language vectors", () => { + it.runIf(writeRequested)("writes the vector file", async () => { + // A computed specifier keeps Node typings out of the Worker typecheck that also covers this file. + const fsModule = "node:fs"; + const fs = (await import(/* @vite-ignore */ fsModule)) as { writeFileSync(path: URL, data: string): void }; + fs.writeFileSync(new URL("./fixtures/release-manifest-v1.json", import.meta.url), `${JSON.stringify(await buildVectors(), null, 2)}\n`); + }); + + it.skipIf(writeRequested)("match the committed vector file byte for byte in content", async () => { + expect(committed).toEqual(await buildVectors()); + }); + + it.skipIf(writeRequested)("verify every valid vector with the published test key", async () => { + const keys = new Map([[committed.testKey.keyId, fromBase64(committed.testKey.publicKeyBase64)]]); + expect(committed.valid.length).toBe(2); + for (const vector of committed.valid) { + expect(await verifyReleaseManifest(vector.manifest, keys), vector.name).toEqual({ valid: true, issues: [] }); + expect(decoder.decode(releaseSigningPayload(vector.manifest))).toBe(vector.signingPayload); + expect(Object.keys(vector.fileContentsBase64)).toEqual(vector.manifest.files.map((f) => f.path)); + } + }); + + it.skipIf(writeRequested)("hash every vector file to its manifest entry", async () => { + for (const vector of committed.valid) { + for (const file of vector.manifest.files) { + const bytes = fromBase64(vector.fileContentsBase64[file.path]!); + expect(bytes.length, file.path).toBe(file.size); + expect(hex(await sha256(bytes)), file.path).toBe(file.sha256); + } + } + }); + + it.skipIf(writeRequested)("reject every invalid-signature vector with its code", async () => { + const keys = new Map([[committed.testKey.keyId, fromBase64(committed.testKey.publicKeyBase64)]]); + for (const vector of committed.invalidSignature) { + expect(validateReleaseManifest(vector.manifest).valid, vector.name).toBe(true); + const result = await verifyReleaseManifest(vector.manifest, keys); + expect(result.issues.map((i) => i.code), vector.name).toEqual([vector.expectedCode]); + } + }); + + it.skipIf(writeRequested)("reject every invalid-manifest vector with its code", () => { + expect(committed.invalidManifest.length).toBeGreaterThan(10); + for (const vector of committed.invalidManifest) { + const result = validateReleaseManifest(vector.manifest); + expect(result.valid, vector.name).toBe(false); + expect(result.issues.map((i) => i.code), vector.name).toContain(vector.expectedCode); + } + }); +}); diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index cecfceb..9578260 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -3,13 +3,14 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", - "lib": ["ES2022"], + "lib": ["ES2022", "DOM"], "strict": true, "noEmit": true, "skipLibCheck": true, "verbatimModuleSyntax": true, "isolatedModules": true, - "noUncheckedIndexedAccess": true + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true }, "include": ["src/**/*"] } diff --git a/packages/skill-tree/test/release-manifest-vectors.test.ts b/packages/skill-tree/test/release-manifest-vectors.test.ts new file mode 100644 index 0000000..7845b70 --- /dev/null +++ b/packages/skill-tree/test/release-manifest-vectors.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import * as root from "../src/index"; +import * as rules from "../src/rules"; +import { LIMITS, treeDigest } from "../src/index"; +import type { TreeFile } from "../src/index"; +import { sha256Hex } from "../src/serialize"; +// The release contract lives in @skillx/contracts, which depends on this package, so the +// guard that both packages agree on the tree imports it by path instead of by name. +import { RELEASE_TREE_LIMITS } from "../../contracts/src/release-manifest"; +import vectorFile from "../../contracts/src/fixtures/release-manifest-v1.json"; + +interface ReleaseVector { + name: string; + manifest: { releaseDigest: string; files: TreeFile[]; fileCount: number; treeSize: number }; + fileContentsBase64: Record; +} + +const vectors = (vectorFile as unknown as { valid: ReleaseVector[] }).valid; + +describe("skillx.release/v1 vectors against agentkit.skill-tree/v1", () => { + it("has vectors to check", () => { + expect(vectors.length).toBeGreaterThan(0); + }); + + it.each(vectors.map((v) => [v.name, v] as const))("%s: releaseDigest is the tree digest of its files", (_name, vector) => { + const files: TreeFile[] = vector.manifest.files.map((f) => { + const bytes = Buffer.from(vector.fileContentsBase64[f.path] ?? "", "base64"); + return { path: f.path, mode: f.mode, size: bytes.length, sha256: sha256Hex(bytes) }; + }); + expect(files).toEqual(vector.manifest.files); + expect(treeDigest(files)).toBe(vector.manifest.releaseDigest); + expect(vector.manifest.fileCount).toBe(files.length); + expect(vector.manifest.treeSize).toBe(files.reduce((sum, f) => sum + f.size, 0)); + }); + + it("keeps the release limits equal to the tree limits", () => { + expect(RELEASE_TREE_LIMITS).toEqual({ maxFiles: LIMITS.maxFiles, maxFileSize: LIMITS.maxFileSize, maxTreeSize: LIMITS.maxTreeSize }); + }); + + it("exposes the same rule implementations from the rules entry point", () => { + expect(rules.validatePath).toBe(root.validatePath); + expect(rules.checkPublisher).toBe(root.checkPublisher); + expect(rules.foldKey).toBe(root.foldKey); + expect(rules.SkillTreeError).toBe(root.SkillTreeError); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf18901..fc91732 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,10 @@ importers: version: 5.9.3 packages/contracts: + dependencies: + '@skillx/skill-tree': + specifier: workspace:* + version: link:../skill-tree devDependencies: typescript: specifier: ^5.9.2 From 77d1acd80c1a782c749310f4fadf12d950ea1827 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 11:50:56 +0700 Subject: [PATCH 08/14] feat(web): add additive migration for release tree digests Add release_digest, file_count, tree_size, signed_manifest_key and quarantined_at to package_releases, packages.visibility and publishers.state with safe defaults, and a release digest index. The generator re-emitted the registry tables missing from the 0010 snapshot; the SQL keeps only the additive delta and records rollback SQL. --- apps/web/app/lib/db/registry-schema.ts | 9 + .../migrations/0011_release_tree_digest.sql | 31 + .../migrations/meta/0011_snapshot.json | 1990 +++++++++++++++++ .../web/drizzle/migrations/meta/_journal.json | 7 + .../db/release-tree-digest-migration.test.ts | 168 ++ docs/api-reference.md | 65 + 6 files changed, 2270 insertions(+) create mode 100644 apps/web/drizzle/migrations/0011_release_tree_digest.sql create mode 100644 apps/web/drizzle/migrations/meta/0011_snapshot.json create mode 100644 apps/web/tests/db/release-tree-digest-migration.test.ts diff --git a/apps/web/app/lib/db/registry-schema.ts b/apps/web/app/lib/db/registry-schema.ts index 38e52eb..47e5c4d 100644 --- a/apps/web/app/lib/db/registry-schema.ts +++ b/apps/web/app/lib/db/registry-schema.ts @@ -8,6 +8,7 @@ export const publishers = sqliteTable( handle: text("handle").notNull().unique(), display_name: text("display_name").notNull(), owner_user_id: text("owner_user_id"), + state: text("state").notNull().default("active"), // active | suspended | revoked created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(), }, (table) => [index("idx_publishers_owner").on(table.owner_user_id)] @@ -24,6 +25,7 @@ export const packages = sqliteTable( name: text("name").notNull(), // fully-qualified '@handle/slug' slug: text("slug").notNull(), kind: text("kind").notNull(), // skill | hook-pack | bundle + visibility: text("visibility").notNull().default("public"), // public | private created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(), updated_at: integer("updated_at", { mode: "timestamp_ms" }).notNull(), }, @@ -48,15 +50,22 @@ export const packageReleases = sqliteTable( artifact_key: text("artifact_key"), // R2 object key, populated by Phase 2 artifact_digest: text("artifact_digest"), // sha256:<64 hex> artifact_size: integer("artifact_size"), + release_digest: text("release_digest"), // treeDigest of the file tree: sha256:<64 hex> + file_count: integer("file_count"), + tree_size: integer("tree_size"), + signed_manifest_key: text("signed_manifest_key"), // R2 key of the signed skillx.release/v1 manifest source_provenance: text("source_provenance"), // JSON: repo, ref, commit compatibility_json: text("compatibility_json"), // declared `compatible` map as JSON created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(), published_at: integer("published_at", { mode: "timestamp_ms" }), yanked_at: integer("yanked_at", { mode: "timestamp_ms" }), + quarantined_at: integer("quarantined_at", { mode: "timestamp_ms" }), }, (table) => [ uniqueIndex("idx_package_releases_version").on(table.package_id, table.version), index("idx_package_releases_state").on(table.release_state), + // Not unique: one tree may be released under several versions or packages. + index("idx_package_releases_digest").on(table.release_digest), ] ); diff --git a/apps/web/drizzle/migrations/0011_release_tree_digest.sql b/apps/web/drizzle/migrations/0011_release_tree_digest.sql new file mode 100644 index 0000000..5bf1a0b --- /dev/null +++ b/apps/web/drizzle/migrations/0011_release_tree_digest.sql @@ -0,0 +1,31 @@ +-- Release tree digest, signed manifest pointer, publisher state and package visibility (issue #54) +-- +-- Strictly additive: nullable columns, or NOT NULL columns with a default, plus one index. +-- Existing rows keep every value; `publishers.state` becomes 'active' and +-- `packages.visibility` becomes 'public'. `release_digest` is the +-- agentkit.skill-tree/v1 tree digest named by the signed skillx.release/v1 manifest. +-- +-- NOTE ON DRIFT: `drizzle-kit generate` emitted CREATE TABLE for all seven registry tables +-- (publishers, packages, package_releases, release_verification_evidence, entitlements, +-- collections, collection_revisions), because the 0010 snapshot omits them although 0009 +-- created them. Those statements were removed here; applying them would fail with "table +-- already exists". The 0011 snapshot records the full schema, which realigns the generator. +-- +-- Rollback (SQLite/D1 support DROP COLUMN since 3.35; take a D1 time-travel bookmark first): +-- DROP INDEX IF EXISTS idx_package_releases_digest; +-- ALTER TABLE package_releases DROP COLUMN quarantined_at; +-- ALTER TABLE package_releases DROP COLUMN signed_manifest_key; +-- ALTER TABLE package_releases DROP COLUMN tree_size; +-- ALTER TABLE package_releases DROP COLUMN file_count; +-- ALTER TABLE package_releases DROP COLUMN release_digest; +-- ALTER TABLE packages DROP COLUMN visibility; +-- ALTER TABLE publishers DROP COLUMN state; + +ALTER TABLE `publishers` ADD `state` text DEFAULT 'active' NOT NULL;--> statement-breakpoint +ALTER TABLE `packages` ADD `visibility` text DEFAULT 'public' NOT NULL;--> statement-breakpoint +ALTER TABLE `package_releases` ADD `release_digest` text;--> statement-breakpoint +ALTER TABLE `package_releases` ADD `file_count` integer;--> statement-breakpoint +ALTER TABLE `package_releases` ADD `tree_size` integer;--> statement-breakpoint +ALTER TABLE `package_releases` ADD `signed_manifest_key` text;--> statement-breakpoint +ALTER TABLE `package_releases` ADD `quarantined_at` integer;--> statement-breakpoint +CREATE INDEX `idx_package_releases_digest` ON `package_releases` (`release_digest`); diff --git a/apps/web/drizzle/migrations/meta/0011_snapshot.json b/apps/web/drizzle/migrations/meta/0011_snapshot.json new file mode 100644 index 0000000..afd591b --- /dev/null +++ b/apps/web/drizzle/migrations/meta/0011_snapshot.json @@ -0,0 +1,1990 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "748d3fcf-9d3f-46a6-8c0b-324ee78d35c3", + "prevId": "22aa723f-5acc-43d2-8f0a-7455bd811d41", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Default'" + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "columns": [ + "key_hash" + ], + "isUnique": true + }, + "idx_api_keys_user": { + "name": "idx_api_keys_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_api_keys_hash": { + "name": "idx_api_keys_hash", + "columns": [ + "key_hash" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "favorites": { + "name": "favorites", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_favorites_pk": { + "name": "idx_favorites_pk", + "columns": [ + "user_id", + "skill_id" + ], + "isUnique": true + }, + "idx_favorites_skill": { + "name": "idx_favorites_skill", + "columns": [ + "skill_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "favorites_skill_id_skills_id_fk": { + "name": "favorites_skill_id_skills_id_fk", + "tableFrom": "favorites", + "tableTo": "skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "installs": { + "name": "installs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_installs_skill": { + "name": "idx_installs_skill", + "columns": [ + "skill_id" + ], + "isUnique": false + }, + "idx_installs_user": { + "name": "idx_installs_user", + "columns": [ + "skill_id", + "user_id" + ], + "isUnique": true, + "where": "user_id IS NOT NULL" + }, + "idx_installs_device": { + "name": "idx_installs_device", + "columns": [ + "skill_id", + "device_id" + ], + "isUnique": true, + "where": "device_id IS NOT NULL" + } + }, + "foreignKeys": { + "installs_skill_id_skills_id_fk": { + "name": "installs_skill_id_skills_id_fk", + "tableFrom": "installs", + "tableTo": "skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ratings": { + "name": "ratings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_agent": { + "name": "is_agent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_ratings_user_skill": { + "name": "idx_ratings_user_skill", + "columns": [ + "user_id", + "skill_id" + ], + "isUnique": true + }, + "idx_ratings_skill": { + "name": "idx_ratings_skill", + "columns": [ + "skill_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ratings_skill_id_skills_id_fk": { + "name": "ratings_skill_id_skills_id_fk", + "tableFrom": "ratings", + "tableTo": "skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reviews": { + "name": "reviews", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_agent": { + "name": "is_agent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_reviews_skill": { + "name": "idx_reviews_skill", + "columns": [ + "skill_id" + ], + "isUnique": false + }, + "idx_reviews_user": { + "name": "idx_reviews_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reviews_skill_id_skills_id_fk": { + "name": "reviews_skill_id_skills_id_fk", + "tableFrom": "reviews", + "tableTo": "skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skill_aliases": { + "name": "skill_aliases", + "columns": { + "slug": { + "name": "slug", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'legacy'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_skill_aliases_skill": { + "name": "idx_skill_aliases_skill", + "columns": [ + "skill_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "skill_aliases_skill_id_skills_id_fk": { + "name": "skill_aliases_skill_id_skills_id_fk", + "tableFrom": "skill_aliases", + "tableTo": "skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "skills": { + "name": "skills", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_repo": { + "name": "source_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "install_command": { + "name": "install_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'1.0.0'" + }, + "is_paid": { + "name": "is_paid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "avg_rating": { + "name": "avg_rating", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "rating_count": { + "name": "rating_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "github_stars": { + "name": "github_stars", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "install_count": { + "name": "install_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "composite_score": { + "name": "composite_score", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "bayesian_rating": { + "name": "bayesian_rating", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "trending_score": { + "name": "trending_score", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "favorite_count": { + "name": "favorite_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "upvote_count": { + "name": "upvote_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "downvote_count": { + "name": "downvote_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "net_votes": { + "name": "net_votes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "scripts": { + "name": "scripts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fts_content": { + "name": "fts_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "risk_label": { + "name": "risk_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "skills_slug_unique": { + "name": "skills_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "idx_skills_category": { + "name": "idx_skills_category", + "columns": [ + "category" + ], + "isUnique": false + }, + "idx_skills_author": { + "name": "idx_skills_author", + "columns": [ + "author" + ], + "isUnique": false + }, + "idx_skills_avg_rating": { + "name": "idx_skills_avg_rating", + "columns": [ + "avg_rating" + ], + "isUnique": false + }, + "idx_skills_composite_score": { + "name": "idx_skills_composite_score", + "columns": [ + "composite_score" + ], + "isUnique": false + }, + "idx_skills_trending_score": { + "name": "idx_skills_trending_score", + "columns": [ + "trending_score" + ], + "isUnique": false + }, + "idx_skills_net_votes": { + "name": "idx_skills_net_votes", + "columns": [ + "net_votes" + ], + "isUnique": false + }, + "idx_skills_source_identity": { + "name": "idx_skills_source_identity", + "columns": [ + "source_repo", + "source_path" + ], + "isUnique": true, + "where": "source_repo IS NOT NULL" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "usage_stats": { + "name": "usage_stats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_usage_skill": { + "name": "idx_usage_skill", + "columns": [ + "skill_id" + ], + "isUnique": false + }, + "idx_usage_created": { + "name": "idx_usage_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "usage_stats_skill_id_skills_id_fk": { + "name": "usage_stats_skill_id_skills_id_fk", + "tableFrom": "usage_stats", + "tableTo": "skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "votes": { + "name": "votes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vote_type": { + "name": "vote_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_votes_user_skill": { + "name": "idx_votes_user_skill", + "columns": [ + "user_id", + "skill_id" + ], + "isUnique": true + }, + "idx_votes_skill": { + "name": "idx_votes_skill", + "columns": [ + "skill_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "votes_skill_id_skills_id_fk": { + "name": "votes_skill_id_skills_id_fk", + "tableFrom": "votes", + "tableTo": "skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "package_releases": { + "name": "package_releases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "package_id": { + "name": "package_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'stable'" + }, + "release_state": { + "name": "release_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "manifest_schema_version": { + "name": "manifest_schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "artifact_key": { + "name": "artifact_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_digest": { + "name": "artifact_digest", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artifact_size": { + "name": "artifact_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "release_digest": { + "name": "release_digest", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tree_size": { + "name": "tree_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signed_manifest_key": { + "name": "signed_manifest_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_provenance": { + "name": "source_provenance", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compatibility_json": { + "name": "compatibility_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "yanked_at": { + "name": "yanked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quarantined_at": { + "name": "quarantined_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_package_releases_version": { + "name": "idx_package_releases_version", + "columns": [ + "package_id", + "version" + ], + "isUnique": true + }, + "idx_package_releases_state": { + "name": "idx_package_releases_state", + "columns": [ + "release_state" + ], + "isUnique": false + }, + "idx_package_releases_digest": { + "name": "idx_package_releases_digest", + "columns": [ + "release_digest" + ], + "isUnique": false + } + }, + "foreignKeys": { + "package_releases_package_id_packages_id_fk": { + "name": "package_releases_package_id_packages_id_fk", + "tableFrom": "package_releases", + "tableTo": "packages", + "columnsFrom": [ + "package_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "packages": { + "name": "packages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'public'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_packages_name": { + "name": "idx_packages_name", + "columns": [ + "name" + ], + "isUnique": true + }, + "idx_packages_publisher": { + "name": "idx_packages_publisher", + "columns": [ + "publisher_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "packages_publisher_id_publishers_id_fk": { + "name": "packages_publisher_id_publishers_id_fk", + "tableFrom": "packages", + "tableTo": "publishers", + "columnsFrom": [ + "publisher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "publishers": { + "name": "publishers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "publishers_handle_unique": { + "name": "publishers_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + }, + "idx_publishers_owner": { + "name": "idx_publishers_owner", + "columns": [ + "owner_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "release_verification_evidence": { + "name": "release_verification_evidence", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "release_digest": { + "name": "release_digest", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "harness_version": { + "name": "harness_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "probe_id": { + "name": "probe_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verified_at": { + "name": "verified_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verifier": { + "name": "verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_release_evidence_release": { + "name": "idx_release_evidence_release", + "columns": [ + "release_id" + ], + "isUnique": false + }, + "idx_release_evidence_probe": { + "name": "idx_release_evidence_probe", + "columns": [ + "release_id", + "harness", + "probe_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "release_verification_evidence_release_id_package_releases_id_fk": { + "name": "release_verification_evidence_release_id_package_releases_id_fk", + "tableFrom": "release_verification_evidence", + "tableTo": "package_releases", + "columnsFrom": [ + "release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entitlements": { + "name": "entitlements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "package_id": { + "name": "package_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order_id": { + "name": "order_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_entitlements_subject": { + "name": "idx_entitlements_subject", + "columns": [ + "subject_id" + ], + "isUnique": false + }, + "idx_entitlements_package": { + "name": "idx_entitlements_package", + "columns": [ + "package_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "entitlements_package_id_packages_id_fk": { + "name": "entitlements_package_id_packages_id_fk", + "tableFrom": "entitlements", + "tableTo": "packages", + "columnsFrom": [ + "package_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entitlements_release_id_package_releases_id_fk": { + "name": "entitlements_release_id_package_releases_id_fk", + "tableFrom": "entitlements", + "tableTo": "package_releases", + "columnsFrom": [ + "release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "collection_revisions": { + "name": "collection_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "collection_id": { + "name": "collection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "members_json": { + "name": "members_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_collection_revisions_number": { + "name": "idx_collection_revisions_number", + "columns": [ + "collection_id", + "revision_number" + ], + "isUnique": true + }, + "idx_collection_revisions_collection": { + "name": "idx_collection_revisions_collection", + "columns": [ + "collection_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "collection_revisions_collection_id_collections_id_fk": { + "name": "collection_revisions_collection_id_collections_id_fk", + "tableFrom": "collection_revisions", + "tableTo": "collections", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "collections": { + "name": "collections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_collections_owner_slug": { + "name": "idx_collections_owner_slug", + "columns": [ + "owner_id", + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/web/drizzle/migrations/meta/_journal.json b/apps/web/drizzle/migrations/meta/_journal.json index d37d4cb..19e812e 100644 --- a/apps/web/drizzle/migrations/meta/_journal.json +++ b/apps/web/drizzle/migrations/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1789982954304, "tag": "0010_sour_runaways", "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1791866720909, + "tag": "0011_release_tree_digest", + "breakpoints": true } ] } diff --git a/apps/web/tests/db/release-tree-digest-migration.test.ts b/apps/web/tests/db/release-tree-digest-migration.test.ts new file mode 100644 index 0000000..681ed57 --- /dev/null +++ b/apps/web/tests/db/release-tree-digest-migration.test.ts @@ -0,0 +1,168 @@ +import { describe, expect, it } from "vitest"; +import { DatabaseSync } from "node:sqlite"; +import { readdirSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { join } from "node:path"; + +/** + * Migration 0011 (release tree digest, publisher state, package visibility) must be strictly + * additive: it applies to a fresh database and to a database at 0010 with rows, leaves every + * existing value unchanged, and its recorded rollback SQL restores the 0010 shape. + */ +const MIGRATIONS_DIR = fileURLToPath(new URL("../../drizzle/migrations", import.meta.url)); +const MIGRATION_0011 = "0011_release_tree_digest.sql"; + +const migrationFiles = readdirSync(MIGRATIONS_DIR) + .filter((file) => file.endsWith(".sql")) + .sort(); + +function statementsOf(file: string): string[] { + return readFileSync(join(MIGRATIONS_DIR, file), "utf-8") + .split("--> statement-breakpoint") + .map((statement) => statement.trim()) + .filter((statement) => statement !== ""); +} + +function applyMigrations(raw: DatabaseSync, files: string[]) { + for (const file of files) for (const statement of statementsOf(file)) raw.exec(statement); +} + +/** Statements with their comment lines removed. */ +function executableSql(file: string): string[] { + return statementsOf(file) + .map((statement) => + statement + .split("\n") + .filter((line) => !line.trim().startsWith("--")) + .join("\n") + .trim(), + ) + .filter((statement) => statement !== ""); +} + +/** The rollback statements recorded in the migration header comment. */ +function rollbackSql(file: string): string[] { + const lines = readFileSync(join(MIGRATIONS_DIR, file), "utf-8").split("\n"); + const start = lines.findIndex((line) => line.startsWith("-- Rollback")); + expect(start, "migration records rollback SQL").toBeGreaterThanOrEqual(0); + const out: string[] = []; + for (const line of lines.slice(start + 1)) { + const match = /^--\s{3}(.+;)\s*$/.exec(line); + if (match) out.push(match[1]!); + else if (!line.startsWith("--")) break; + } + return out; +} + +type Row = Record; + +function columns(raw: DatabaseSync, table: string): Row[] { + return raw.prepare(`PRAGMA table_info(${table})`).all() as Row[]; +} + +function indexes(raw: DatabaseSync, table: string): string[] { + return (raw.prepare(`PRAGMA index_list(${table})`).all() as Array<{ name: string }>).map((i) => i.name).sort(); +} + +const REGISTRY_TABLES = ["publishers", "packages", "package_releases"] as const; + +function snapshot(raw: DatabaseSync, columnsByTable: Record) { + return Object.fromEntries( + REGISTRY_TABLES.map((table) => { + const cols = columnsByTable[table]!.map((c) => `\`${c}\``).join(", "); + return [table, raw.prepare(`SELECT ${cols} FROM ${table} ORDER BY id`).all()]; + }), + ); +} + +function seedRegistry(raw: DatabaseSync) { + raw.exec(` + INSERT INTO publishers (id, handle, display_name, owner_user_id, created_at) VALUES + ('pub-1', 'publisher', 'Publisher', 'user-1', 1000), + ('pub-2', 'solo.dev', 'Solo', NULL, 2000); + INSERT INTO packages (id, publisher_id, name, slug, kind, created_at, updated_at) VALUES + ('pkg-1', 'pub-1', '@publisher/review-guardrails', 'review-guardrails', 'skill', 1000, 1100), + ('pkg-2', 'pub-2', '@solo.dev/tiny', 'tiny', 'hook-pack', 2000, 2100); + INSERT INTO package_releases (id, package_id, version, channel, release_state, manifest_schema_version, + artifact_key, artifact_digest, artifact_size, source_provenance, compatibility_json, created_at, published_at, yanked_at) VALUES + ('rel-1', 'pkg-1', '1.0.0', 'stable', 'published', 'skillx.package/v1', + 'artifacts/sha256/${"a".repeat(64)}.tar', 'sha256:${"a".repeat(64)}', 4096, '{"repo":"x"}', '{}', 1200, 1300, NULL), + ('rel-2', 'pkg-1', '1.1.0', 'beta', 'yanked', 'skillx.package/v1', NULL, NULL, NULL, NULL, NULL, 1400, NULL, 1500), + ('rel-3', 'pkg-2', '0.1.0', 'stable', 'draft', 'skillx.package/v1', NULL, NULL, NULL, NULL, NULL, 2200, NULL, NULL); + `); +} + +describe("migration 0011_release_tree_digest", () => { + it("is the latest migration", () => { + expect(migrationFiles.at(-1)).toBe(MIGRATION_0011); + }); + + it("contains only additive statements", () => { + const statements = executableSql(MIGRATION_0011); + expect(statements.length).toBeGreaterThan(0); + for (const statement of statements) { + expect(statement, statement).toMatch(/^(ALTER TABLE `\w+` ADD `\w+` [^;]+;|CREATE INDEX `\w+` ON `\w+` \([^)]*\);)$/); + expect(statement).not.toMatch(/\b(DROP|DELETE|UPDATE|RENAME|INSERT)\b/i); + // A NOT NULL column added to a table with rows needs a default. + if (/NOT NULL/.test(statement)) expect(statement).toMatch(/DEFAULT '[a-z]+' NOT NULL/); + } + }); + + it("applies to a fresh database with the documented defaults", () => { + const raw = new DatabaseSync(":memory:"); + applyMigrations(raw, migrationFiles); + const byName = (table: string) => Object.fromEntries(columns(raw, table).map((c) => [c.name, c])); + expect(byName("publishers").state).toMatchObject({ notnull: 1, dflt_value: "'active'" }); + expect(byName("packages").visibility).toMatchObject({ notnull: 1, dflt_value: "'public'" }); + for (const column of ["release_digest", "file_count", "tree_size", "signed_manifest_key", "quarantined_at"]) { + expect(byName("package_releases")[column], column).toMatchObject({ notnull: 0, dflt_value: null }); + } + expect(indexes(raw, "package_releases")).toContain("idx_package_releases_digest"); + expect(indexes(raw, "package_releases")).toContain("idx_package_releases_version"); + }); + + it("keeps every existing row and value when applied to a database at 0010", () => { + const raw = new DatabaseSync(":memory:"); + applyMigrations(raw, migrationFiles.slice(0, -1)); + seedRegistry(raw); + const columnsBefore = Object.fromEntries(REGISTRY_TABLES.map((t) => [t, columns(raw, t).map((c) => String(c.name))])); + const before = snapshot(raw, columnsBefore); + + applyMigrations(raw, [MIGRATION_0011]); + + expect(snapshot(raw, columnsBefore)).toEqual(before); + for (const table of REGISTRY_TABLES) { + const after = columns(raw, table).map((c) => String(c.name)); + expect(after.slice(0, columnsBefore[table]!.length), table).toEqual(columnsBefore[table]); + } + expect(raw.prepare("SELECT id, state FROM publishers ORDER BY id").all()).toEqual([ + { id: "pub-1", state: "active" }, + { id: "pub-2", state: "active" }, + ]); + expect(raw.prepare("SELECT DISTINCT visibility FROM packages").all()).toEqual([{ visibility: "public" }]); + expect( + raw + .prepare( + "SELECT count(*) AS n FROM package_releases WHERE release_digest IS NULL AND file_count IS NULL AND tree_size IS NULL AND signed_manifest_key IS NULL AND quarantined_at IS NULL", + ) + .get(), + ).toEqual({ n: 3 }); + }); + + it("rolls back to the 0010 shape with the recorded SQL", () => { + const raw = new DatabaseSync(":memory:"); + applyMigrations(raw, migrationFiles.slice(0, -1)); + seedRegistry(raw); + const shapeBefore = Object.fromEntries(REGISTRY_TABLES.map((t) => [t, [columns(raw, t), indexes(raw, t)]])); + const columnsBefore = Object.fromEntries(REGISTRY_TABLES.map((t) => [t, columns(raw, t).map((c) => String(c.name))])); + const rowsBefore = snapshot(raw, columnsBefore); + + applyMigrations(raw, [MIGRATION_0011]); + const rollback = rollbackSql(MIGRATION_0011); + expect(rollback.length).toBeGreaterThan(0); + for (const statement of rollback) raw.exec(statement); + + expect(Object.fromEntries(REGISTRY_TABLES.map((t) => [t, [columns(raw, t), indexes(raw, t)]]))).toEqual(shapeBefore); + expect(snapshot(raw, columnsBefore)).toEqual(rowsBefore); + }); +}); diff --git a/docs/api-reference.md b/docs/api-reference.md index 91bbccf..5239254 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -728,6 +728,71 @@ Bulk upsert skills and index into Vectorize. For seeding demo/production data. --- +## Release Manifest Contract (`skillx.release/v1`) + +Contract only: no endpoint serves or accepts release manifests yet. The +publisher pipeline and pinned read API build on it. Source: +`packages/contracts/src/release-manifest.ts`; canonical JSON: +`packages/contracts/src/canonical-json.ts`. + +A release manifest pins one immutable `@publisher/slug@version` to the +`agentkit.skill-tree/v1` tree of its files. It is a JSON object with exactly +these members; unknown members are rejected at every level. + +| Member | Type | Rule | +|--------|------|------| +| `schemaVersion` | string | `skillx.release/v1` | +| `identity` | string | `PACKAGE_NAME_PATTERN`, publisher and slug pass the tree segment rules; `@agentkit/*` is rejected (`identity.reserved_publisher`) | +| `version` | string | semantic version | +| `channel` | string | `stable`, `beta`, `alpha` | +| `kind` | string | `skill`, `hook-pack`, `bundle` | +| `releaseDigest` | string | `sha256:<64 lowercase hex>`; equals the tree digest of `files` and is the release identity | +| `artifactDigest` | string | `sha256:<64 lowercase hex>` of the transport archive; integrity only, never an identity | +| `artifactSize` | integer | positive | +| `files` | array | non-empty, at most 4096 entries of `{ path, mode, sha256, size }`, sorted by raw UTF-8 path bytes, no duplicates or case-fold collisions; `path` passes the tree path, NFC and noise rules; `mode` is `100644` or `100755`; `sha256` is bare lowercase hex; `size` at most 32 MiB | +| `fileCount` | integer | equals `files.length` | +| `treeSize` | integer | sum of file sizes, at most 256 MiB | +| `state` | string | `draft`, `quarantined`, `published`, `yanked` at signing time; a state change produces a newly signed manifest for the same `releaseDigest` | +| `publisher` | object | `{ handle, verifiedBy }`; `handle` equals the identity publisher; `verifiedBy` is `none`, `github` or `admin` (`none` means not publisher-verified) | +| `sourceProvenance` | object or `null` | `{ repo, commit, path }`: https URL without credentials, full lowercase hex commit, package root path (`""` for the repository root) | +| `declaredScripts` | array | paths from `files`, unique and sorted like `files`; informational, never executed by SkillX | +| `createdAt` | string | RFC 3339 UTC with whole seconds, e.g. `2026-10-13T04:30:00Z` | +| `keyId` | string | `^[a-z0-9][a-z0-9._-]{1,63}$` | +| `signatureAlgorithm` | string | `ed25519` | +| `signature` | string | padded standard base64 of the 64-byte Ed25519 signature | + +### Signature bytes + +The signature covers the UTF-8 bytes of the canonical JSON of the manifest +with only the `signature` member removed (so `keyId` and `signatureAlgorithm` +are signed). Canonical JSON is the RFC 8785 form restricted to this value +space: + +- object members sorted by key (UTF-16 code units; every key is ASCII), no whitespace anywhere; +- arrays keep their order; numbers are non-negative safe integers in plain decimal; +- strings escape only `"` (`\"`), `\` (`\\`), U+0008/09/0A/0C/0D (`\b \t \n \f \r`) and other + code points below U+0020 (`\u00xx`, lowercase hex); everything else, including `/`, `<`, + `>`, `&`, U+2028 and U+2029, is raw UTF-8; +- no BOM, no trailing newline. + +Go's `encoding/json.Marshal` does not produce these bytes (it HTML-escapes and +escapes U+2028/U+2029); a verifier must re-encode with the rules above. + +Cross-language vectors: `packages/contracts/src/fixtures/release-manifest-v1.json` +(signed valid manifests with their exact signing payload and file contents, +invalid-signature cases, invalid-manifest cases). The vector key is derived +from a public seed text and must never be trusted outside tests. + +### Database columns (migration `0011_release_tree_digest`) + +`package_releases`: `release_digest`, `file_count`, `tree_size`, +`signed_manifest_key` (R2 key of the signed manifest), `quarantined_at`, and the +non-unique index `idx_package_releases_digest`. `packages.visibility` +(`public` default, `private`). `publishers.state` (`active` default, +`suspended`, `revoked`). All additive; rollback SQL is in the migration header. + +--- + ## Error Format All error responses follow a consistent shape: From b0e45b72e64d731b6b1ead14949ebae5c2df416d Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 12:03:26 +0700 Subject: [PATCH 09/14] feat(contracts): sign release state in a separate status document Keep the skillx.release/v1 manifest immutable per release digest and move the mutable state into skillx.release-status/v1: release digest, state, optional reason, issuedAt, a strictly increasing sequence and the signing key, under the same canonical bytes and Ed25519 rules. Verification fails closed on an unknown state, a status for another release, a bad signature, a lower sequence (rollback) and a different status with the accepted sequence (conflict). Ship cross-language status vectors, regenerate the manifest vectors without state, and record the last status sequence and issue time in migration 0011. Document that a release without source provenance must be labeled provenance unknown. --- apps/web/app/lib/db/registry-schema.ts | 3 + .../migrations/0011_release_tree_digest.sql | 6 + .../migrations/meta/0011_snapshot.json | 14 + .../db/release-tree-digest-migration.test.ts | 12 +- docs/api-reference.md | 47 ++- packages/contracts/src/fixtures/index.ts | 3 + .../src/fixtures/release-fixtures.ts | 10 +- .../src/fixtures/release-manifest-v1.json | 72 +--- .../src/fixtures/release-status-fixtures.ts | 101 +++++ .../src/fixtures/release-status-v1.json | 344 ++++++++++++++++++ packages/contracts/src/fixtures/types.ts | 3 +- packages/contracts/src/index.ts | 1 + .../contracts/src/package-manifest.test.ts | 9 +- .../contracts/src/release-manifest.test.ts | 9 +- packages/contracts/src/release-manifest.ts | 152 ++------ packages/contracts/src/release-signature.ts | 137 +++++++ .../src/release-status-vectors.test.ts | 198 ++++++++++ packages/contracts/src/release-status.test.ts | 168 +++++++++ packages/contracts/src/release-status.ts | 185 ++++++++++ .../contracts/src/release-vectors.test.ts | 108 ++---- .../contracts/src/test-support/vector-key.ts | 76 ++++ 21 files changed, 1399 insertions(+), 259 deletions(-) create mode 100644 packages/contracts/src/fixtures/release-status-fixtures.ts create mode 100644 packages/contracts/src/fixtures/release-status-v1.json create mode 100644 packages/contracts/src/release-signature.ts create mode 100644 packages/contracts/src/release-status-vectors.test.ts create mode 100644 packages/contracts/src/release-status.test.ts create mode 100644 packages/contracts/src/release-status.ts create mode 100644 packages/contracts/src/test-support/vector-key.ts diff --git a/apps/web/app/lib/db/registry-schema.ts b/apps/web/app/lib/db/registry-schema.ts index 47e5c4d..a13bfd1 100644 --- a/apps/web/app/lib/db/registry-schema.ts +++ b/apps/web/app/lib/db/registry-schema.ts @@ -60,6 +60,9 @@ export const packageReleases = sqliteTable( published_at: integer("published_at", { mode: "timestamp_ms" }), yanked_at: integer("yanked_at", { mode: "timestamp_ms" }), quarantined_at: integer("quarantined_at", { mode: "timestamp_ms" }), + // Last signed skillx.release-status/v1 document: strictly increasing sequence and its issuedAt. + status_sequence: integer("status_sequence"), + status_issued_at: integer("status_issued_at", { mode: "timestamp_ms" }), }, (table) => [ uniqueIndex("idx_package_releases_version").on(table.package_id, table.version), diff --git a/apps/web/drizzle/migrations/0011_release_tree_digest.sql b/apps/web/drizzle/migrations/0011_release_tree_digest.sql index 5bf1a0b..8d069ba 100644 --- a/apps/web/drizzle/migrations/0011_release_tree_digest.sql +++ b/apps/web/drizzle/migrations/0011_release_tree_digest.sql @@ -4,6 +4,8 @@ -- Existing rows keep every value; `publishers.state` becomes 'active' and -- `packages.visibility` becomes 'public'. `release_digest` is the -- agentkit.skill-tree/v1 tree digest named by the signed skillx.release/v1 manifest. +-- `status_sequence` and `status_issued_at` record the last signed skillx.release-status/v1 +-- document, whose sequence must strictly increase. -- -- NOTE ON DRIFT: `drizzle-kit generate` emitted CREATE TABLE for all seven registry tables -- (publishers, packages, package_releases, release_verification_evidence, entitlements, @@ -13,6 +15,8 @@ -- -- Rollback (SQLite/D1 support DROP COLUMN since 3.35; take a D1 time-travel bookmark first): -- DROP INDEX IF EXISTS idx_package_releases_digest; +-- ALTER TABLE package_releases DROP COLUMN status_issued_at; +-- ALTER TABLE package_releases DROP COLUMN status_sequence; -- ALTER TABLE package_releases DROP COLUMN quarantined_at; -- ALTER TABLE package_releases DROP COLUMN signed_manifest_key; -- ALTER TABLE package_releases DROP COLUMN tree_size; @@ -28,4 +32,6 @@ ALTER TABLE `package_releases` ADD `file_count` integer;--> statement-breakpoint ALTER TABLE `package_releases` ADD `tree_size` integer;--> statement-breakpoint ALTER TABLE `package_releases` ADD `signed_manifest_key` text;--> statement-breakpoint ALTER TABLE `package_releases` ADD `quarantined_at` integer;--> statement-breakpoint +ALTER TABLE `package_releases` ADD `status_sequence` integer;--> statement-breakpoint +ALTER TABLE `package_releases` ADD `status_issued_at` integer;--> statement-breakpoint CREATE INDEX `idx_package_releases_digest` ON `package_releases` (`release_digest`); diff --git a/apps/web/drizzle/migrations/meta/0011_snapshot.json b/apps/web/drizzle/migrations/meta/0011_snapshot.json index afd591b..09b8d84 100644 --- a/apps/web/drizzle/migrations/meta/0011_snapshot.json +++ b/apps/web/drizzle/migrations/meta/0011_snapshot.json @@ -1396,6 +1396,20 @@ "primaryKey": false, "notNull": false, "autoincrement": false + }, + "status_sequence": { + "name": "status_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_issued_at": { + "name": "status_issued_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false } }, "indexes": { diff --git a/apps/web/tests/db/release-tree-digest-migration.test.ts b/apps/web/tests/db/release-tree-digest-migration.test.ts index 681ed57..7e547a2 100644 --- a/apps/web/tests/db/release-tree-digest-migration.test.ts +++ b/apps/web/tests/db/release-tree-digest-migration.test.ts @@ -114,7 +114,15 @@ describe("migration 0011_release_tree_digest", () => { const byName = (table: string) => Object.fromEntries(columns(raw, table).map((c) => [c.name, c])); expect(byName("publishers").state).toMatchObject({ notnull: 1, dflt_value: "'active'" }); expect(byName("packages").visibility).toMatchObject({ notnull: 1, dflt_value: "'public'" }); - for (const column of ["release_digest", "file_count", "tree_size", "signed_manifest_key", "quarantined_at"]) { + for (const column of [ + "release_digest", + "file_count", + "tree_size", + "signed_manifest_key", + "quarantined_at", + "status_sequence", + "status_issued_at", + ]) { expect(byName("package_releases")[column], column).toMatchObject({ notnull: 0, dflt_value: null }); } expect(indexes(raw, "package_releases")).toContain("idx_package_releases_digest"); @@ -143,7 +151,7 @@ describe("migration 0011_release_tree_digest", () => { expect( raw .prepare( - "SELECT count(*) AS n FROM package_releases WHERE release_digest IS NULL AND file_count IS NULL AND tree_size IS NULL AND signed_manifest_key IS NULL AND quarantined_at IS NULL", + "SELECT count(*) AS n FROM package_releases WHERE release_digest IS NULL AND file_count IS NULL AND tree_size IS NULL AND signed_manifest_key IS NULL AND quarantined_at IS NULL AND status_sequence IS NULL AND status_issued_at IS NULL", ) .get(), ).toEqual({ n: 3 }); diff --git a/docs/api-reference.md b/docs/api-reference.md index 5239254..532b6c4 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -736,8 +736,10 @@ publisher pipeline and pinned read API build on it. Source: `packages/contracts/src/canonical-json.ts`. A release manifest pins one immutable `@publisher/slug@version` to the -`agentkit.skill-tree/v1` tree of its files. It is a JSON object with exactly -these members; unknown members are rejected at every level. +`agentkit.skill-tree/v1` tree of its files. It is signed once and never +changes; the release lifecycle state is a separate signed document +(`skillx.release-status/v1`, below). It is a JSON object with exactly these +members; unknown members (including `state`) are rejected at every level. | Member | Type | Rule | |--------|------|------| @@ -752,9 +754,8 @@ these members; unknown members are rejected at every level. | `files` | array | non-empty, at most 4096 entries of `{ path, mode, sha256, size }`, sorted by raw UTF-8 path bytes, no duplicates or case-fold collisions; `path` passes the tree path, NFC and noise rules; `mode` is `100644` or `100755`; `sha256` is bare lowercase hex; `size` at most 32 MiB | | `fileCount` | integer | equals `files.length` | | `treeSize` | integer | sum of file sizes, at most 256 MiB | -| `state` | string | `draft`, `quarantined`, `published`, `yanked` at signing time; a state change produces a newly signed manifest for the same `releaseDigest` | | `publisher` | object | `{ handle, verifiedBy }`; `handle` equals the identity publisher; `verifiedBy` is `none`, `github` or `admin` (`none` means not publisher-verified) | -| `sourceProvenance` | object or `null` | `{ repo, commit, path }`: https URL without credentials, full lowercase hex commit, package root path (`""` for the repository root) | +| `sourceProvenance` | object or `null` | `{ repo, commit, path }`: https URL without credentials, full lowercase hex commit, package root path (`""` for the repository root). `null` when the release was not packed from a recorded git commit; consumers must label such a release "provenance unknown" | | `declaredScripts` | array | paths from `files`, unique and sorted like `files`; informational, never executed by SkillX | | `createdAt` | string | RFC 3339 UTC with whole seconds, e.g. `2026-10-13T04:30:00Z` | | `keyId` | string | `^[a-z0-9][a-z0-9._-]{1,63}$` | @@ -783,11 +784,45 @@ Cross-language vectors: `packages/contracts/src/fixtures/release-manifest-v1.jso invalid-signature cases, invalid-manifest cases). The vector key is derived from a public seed text and must never be trusted outside tests. +### Release status (`skillx.release-status/v1`) + +The mutable state of a release is a separately signed document with the same +signature bytes rules (canonical JSON without `signature`, Ed25519, padded +base64). Source: `packages/contracts/src/release-status.ts`. + +| Member | Type | Rule | +|--------|------|------| +| `schemaVersion` | string | `skillx.release-status/v1` | +| `releaseDigest` | string | `releaseDigest` of the manifest the status describes | +| `state` | string | `draft`, `quarantined`, `published`, `yanked` | +| `reason` | string, optional | 1 to 512 characters, no control characters; omit the member when there is none (never `null`) | +| `issuedAt` | string | RFC 3339 UTC with whole seconds | +| `sequence` | integer | strictly increasing per release, starting at 1 | +| `keyId` | string | `^[a-z0-9][a-z0-9._-]{1,63}$` | +| `signatureAlgorithm` | string | `ed25519` | +| `signature` | string | padded standard base64 of the 64-byte Ed25519 signature | + +A consumer checks, in this order, and fails closed: + +1. structure; a `state` outside the list above is `status.unknown_state`, even when correctly signed; +2. `releaseDigest` equals the verified manifest's `releaseDigest`, else `status.digest_mismatch`; +3. the signature (`signature.unknown_key`, `signature.invalid_key`, `signature.mismatch`); +4. against the last status it accepted for the release: a lower `sequence` is + `status.rollback`; the same `sequence` with different signed bytes is + `status.conflict`; identical bytes are accepted. + +Cross-language vectors: `packages/contracts/src/fixtures/release-status-v1.json` +(valid statuses with their signing payload and the previously accepted status; +tampered, wrong-key, unknown-key, Go-escaped, rollback, conflict, unknown-state +and other-release cases). Same public test key as the manifest vectors. + ### Database columns (migration `0011_release_tree_digest`) `package_releases`: `release_digest`, `file_count`, `tree_size`, -`signed_manifest_key` (R2 key of the signed manifest), `quarantined_at`, and the -non-unique index `idx_package_releases_digest`. `packages.visibility` +`signed_manifest_key` (R2 key of the signed manifest), `quarantined_at`, +`status_sequence` and `status_issued_at` (sequence and issue time of the last +signed status; the state itself stays in `release_state`), and the non-unique +index `idx_package_releases_digest`. `packages.visibility` (`public` default, `private`). `publishers.state` (`active` default, `suspended`, `revoked`). All additive; rollback SQL is in the migration header. diff --git a/packages/contracts/src/fixtures/index.ts b/packages/contracts/src/fixtures/index.ts index ecf2d7a..7f3ec9d 100644 --- a/packages/contracts/src/fixtures/index.ts +++ b/packages/contracts/src/fixtures/index.ts @@ -12,12 +12,14 @@ import { INGESTION_FIXTURES, } from "./collection-fixtures"; import { RELEASE_MANIFEST_FIXTURES } from "./release-fixtures"; +import { RELEASE_STATUS_FIXTURES } from "./release-status-fixtures"; import type { FixtureCase } from "./types"; export * from "./types"; export * from "./manifest-fixtures"; export * from "./collection-fixtures"; export * from "./release-fixtures"; +export * from "./release-status-fixtures"; export const ALL_FIXTURES: FixtureCase[] = [ ...PACKAGE_MANIFEST_FIXTURES, @@ -26,4 +28,5 @@ export const ALL_FIXTURES: FixtureCase[] = [ ...ENTITLEMENT_FIXTURES, ...INGESTION_FIXTURES, ...RELEASE_MANIFEST_FIXTURES, + ...RELEASE_STATUS_FIXTURES, ]; diff --git a/packages/contracts/src/fixtures/release-fixtures.ts b/packages/contracts/src/fixtures/release-fixtures.ts index 3149540..ae4828e 100644 --- a/packages/contracts/src/fixtures/release-fixtures.ts +++ b/packages/contracts/src/fixtures/release-fixtures.ts @@ -35,7 +35,7 @@ export const RELEASE_FIXTURE_CONTENTS: Readonly { d.files[0]!.sha256 = String(d.files[0]!.sha256).toUpperCase(); }), - rejected("rejects a release state outside the enum", "unsupported_value", (d) => { - d.state = "blocked"; + rejected("rejects a release state member, which belongs to the status document", "unknown_field", (d) => { + d.state = "published"; }), rejected("rejects the reserved @agentkit publisher", "identity.reserved_publisher", (d) => { d.identity = "@agentkit/review-guardrails"; diff --git a/packages/contracts/src/fixtures/release-manifest-v1.json b/packages/contracts/src/fixtures/release-manifest-v1.json index 6735ab6..e3963ad 100644 --- a/packages/contracts/src/fixtures/release-manifest-v1.json +++ b/packages/contracts/src/fixtures/release-manifest-v1.json @@ -10,7 +10,7 @@ }, "valid": [ { - "name": "published multi-file release", + "name": "publisher-verified multi-file release", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@publisher/review-guardrails", @@ -42,7 +42,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -58,10 +57,10 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "X7m1EkEXrg5sIbEcG/d2MPXB4J5o/QsEOYvBMWvzJy0LKAgZN3hqCNZeknJR9XzUdpp4eRtmjPgvalTo+0L3DQ==" + "signature": "eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==" }, - "signingPayload": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"state\":\"published\",\"treeSize\":154,\"version\":\"1.2.0\"}", - "signingPayloadSha256": "2dbd8ec2f29ac3582e0e754efc8b2c3a4766f23f05e173abfd6038ab25242214", + "signingPayload": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "signingPayloadSha256": "64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "fileContentsBase64": { "SKILL.md": "LS0tCm5hbWU6IHJldmlldy1ndWFyZHJhaWxzCmRlc2NyaXB0aW9uOiBSZXZpZXcgZ3VhcmRyYWlscyBmb3IgcHVsbCByZXF1ZXN0cy4KLS0tCiMgUmV2aWV3IGd1YXJkcmFpbHMK", "references/café & notes
.md": "Tm90ZXMgd2l0aCBhIGxpbmUgc2VwYXJhdG9yIHBhdGguCg==", @@ -69,7 +68,7 @@ } }, { - "name": "quarantined minimal prerelease", + "name": "unverified minimal prerelease with unknown provenance", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@solo.dev/tiny_skill", @@ -89,7 +88,6 @@ ], "fileCount": 1, "treeSize": 44, - "state": "quarantined", "publisher": { "handle": "solo.dev", "verifiedBy": "none" @@ -99,10 +97,10 @@ "createdAt": "2026-10-13T05:00:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "4/jiQH7dCgeJ+ZSR52FVtR9FJXDlOoxAwFT5g6HytAe/lN7NpAJDnzBwLXN5biN2jCaJRhUyIG/2UHWuU6n0Dw==" + "signature": "tEUDJBptJIcz6nVFciKaixGz3bUvTe5ZAVI6cYBFtWsK75DU3bTqSfauO8Tf2Te85zfHA3IbY0FOYPviKWS/Dw==" }, - "signingPayload": "{\"artifactDigest\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"artifactSize\":1536,\"channel\":\"beta\",\"createdAt\":\"2026-10-13T05:00:00Z\",\"declaredScripts\":[],\"fileCount\":1,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d\",\"size\":44}],\"identity\":\"@solo.dev/tiny_skill\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"solo.dev\",\"verifiedBy\":\"none\"},\"releaseDigest\":\"sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544\",\"schemaVersion\":\"skillx.release/v1\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":null,\"state\":\"quarantined\",\"treeSize\":44,\"version\":\"0.1.0-beta.1\"}", - "signingPayloadSha256": "c26ee8e521c41bf3e116225e79d3b2a5973e77dfbe22c3f4962b63b1263d4fd7", + "signingPayload": "{\"artifactDigest\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"artifactSize\":1536,\"channel\":\"beta\",\"createdAt\":\"2026-10-13T05:00:00Z\",\"declaredScripts\":[],\"fileCount\":1,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d\",\"size\":44}],\"identity\":\"@solo.dev/tiny_skill\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"solo.dev\",\"verifiedBy\":\"none\"},\"releaseDigest\":\"sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544\",\"schemaVersion\":\"skillx.release/v1\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":null,\"treeSize\":44,\"version\":\"0.1.0-beta.1\"}", + "signingPayloadSha256": "5979df5f8e7c400ac422183440040742bb34274bca37c9347d100948871e3596", "fileContentsBase64": { "SKILL.md": "LS0tCm5hbWU6IHRpbnkKZGVzY3JpcHRpb246IFRpbnkgc2tpbGwuCi0tLQo=" } @@ -110,11 +108,11 @@ ], "invalidSignature": [ { - "name": "state changed after signing", + "name": "version changed after signing", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@publisher/review-guardrails", - "version": "1.2.0", + "version": "1.2.1", "channel": "stable", "kind": "skill", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", @@ -142,7 +140,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "yanked", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -158,7 +155,7 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "X7m1EkEXrg5sIbEcG/d2MPXB4J5o/QsEOYvBMWvzJy0LKAgZN3hqCNZeknJR9XzUdpp4eRtmjPgvalTo+0L3DQ==" + "signature": "eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==" }, "expectedCode": "signature.mismatch" }, @@ -195,7 +192,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -211,7 +207,7 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "Xrm1EkEXrg5sIbEcG/d2MPXB4J5o/QsEOYvBMWvzJy0LKAgZN3hqCNZeknJR9XzUdpp4eRtmjPgvalTo+0L3DQ==" + "signature": "eXdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==" }, "expectedCode": "signature.mismatch" }, @@ -248,7 +244,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -264,7 +259,7 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "xHoZb1jFtQRzh2cNjc16ptow+NB9zuvO7KCdvqqWrBLWwPVAQ8IoS2D550rCj+Usqe4holIV3alFOYfgIL5sDg==" + "signature": "xmFvEJWdvdJjY15sAF4O2YqthxSCz5ZDz6XA0NivxR7YVKbsUEx7bIt6MfrBC+B/RBR+8TxEXwg9zI8wku4kDQ==" }, "expectedCode": "signature.mismatch" }, @@ -301,7 +296,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -317,7 +311,7 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-unknown-key", "signatureAlgorithm": "ed25519", - "signature": "X7m1EkEXrg5sIbEcG/d2MPXB4J5o/QsEOYvBMWvzJy0LKAgZN3hqCNZeknJR9XzUdpp4eRtmjPgvalTo+0L3DQ==" + "signature": "eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==" }, "expectedCode": "signature.unknown_key" }, @@ -354,7 +348,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -370,7 +363,7 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "0/2aFw3vNJA1qTvsyWiwpI4YQVZTo3H43udjoWG+E+H9zZuBiLCgySXpa/qUn01dqm6tJbS3o4ykXjLQFLfSDQ==" + "signature": "9dEh1PX7CiqOi6gaClemhI+a0hF5HGSok+q6zFCsVwn2a3stv8J4WARheAWzRTIHJTwKBGT39lAgKNygoPgRAQ==" }, "expectedCode": "signature.mismatch" } @@ -409,7 +402,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -464,7 +456,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -517,7 +508,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -569,7 +559,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -622,7 +611,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -675,7 +663,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -728,7 +715,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -781,7 +767,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -834,7 +819,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -887,7 +871,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -940,7 +923,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -993,7 +975,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1046,7 +1027,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1067,7 +1047,7 @@ "expectedCode": "pattern" }, { - "name": "rejects a release state outside the enum", + "name": "rejects a release state member, which belongs to the status document", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@publisher/review-guardrails", @@ -1099,7 +1079,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "blocked", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1115,9 +1094,10 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "state": "published" }, - "expectedCode": "unsupported_value" + "expectedCode": "unknown_field" }, { "name": "rejects the reserved @agentkit publisher", @@ -1152,7 +1132,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "agentkit", "verifiedBy": "github" @@ -1205,7 +1184,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1258,7 +1236,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "someone-else", "verifiedBy": "github" @@ -1311,7 +1288,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "self" @@ -1364,7 +1340,6 @@ ], "fileCount": 4, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1417,7 +1392,6 @@ ], "fileCount": 3, "treeSize": 155, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1470,7 +1444,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1523,7 +1496,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1576,7 +1548,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1629,7 +1600,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1682,7 +1652,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1735,7 +1704,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1788,7 +1756,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" @@ -1841,7 +1808,6 @@ ], "fileCount": 3, "treeSize": 154, - "state": "published", "publisher": { "handle": "publisher", "verifiedBy": "github" diff --git a/packages/contracts/src/fixtures/release-status-fixtures.ts b/packages/contracts/src/fixtures/release-status-fixtures.ts new file mode 100644 index 0000000..4d1b0dd --- /dev/null +++ b/packages/contracts/src/fixtures/release-status-fixtures.ts @@ -0,0 +1,101 @@ +/** + * `skillx.release-status/v1` fixtures. Statuses describe the releases of + * `release-fixtures.ts`; the signed cross-language vectors in + * `release-status-v1.json` are built from them. + */ + +import type { SkillxReleaseStatus, UnsignedReleaseStatus } from "../release-status"; +import { RELEASE_FIXTURE_KEY_ID, RELEASE_FIXTURE_PLACEHOLDER_SIGNATURE, RELEASE_MANIFEST_FIXTURE } from "./release-fixtures"; +import type { FixtureCase, MutableFixture } from "./types"; + +/** + * A reason with non-ASCII, `&`, `<` and `>`, which the canonical JSON emits + * raw. Built from code points so no editor can normalize it. + */ +export const RELEASE_STATUS_FIXTURE_REASON = `Yanked: unsafe script & caf${String.fromCodePoint(0xe9)} notes`; + +/** The first status of the full fixture release. */ +export const RELEASE_STATUS_PUBLISHED_FIXTURE: UnsignedReleaseStatus = { + schemaVersion: "skillx.release-status/v1", + releaseDigest: RELEASE_MANIFEST_FIXTURE.releaseDigest, + state: "published", + issuedAt: "2026-10-13T04:31:00Z", + sequence: 1, + keyId: RELEASE_FIXTURE_KEY_ID, + signatureAlgorithm: "ed25519", +}; + +/** A later status of the same release, with a reason. */ +export const RELEASE_STATUS_YANKED_FIXTURE: UnsignedReleaseStatus = { + schemaVersion: "skillx.release-status/v1", + releaseDigest: RELEASE_MANIFEST_FIXTURE.releaseDigest, + state: "yanked", + reason: RELEASE_STATUS_FIXTURE_REASON, + issuedAt: "2026-10-14T09:00:00Z", + sequence: 2, + keyId: RELEASE_FIXTURE_KEY_ID, + signatureAlgorithm: "ed25519", +}; + +function signedStatusDraft(base: UnsignedReleaseStatus): MutableFixture { + const copy = JSON.parse(JSON.stringify(base)) as SkillxReleaseStatus; + copy.signature = RELEASE_FIXTURE_PLACEHOLDER_SIGNATURE; + return copy as unknown as MutableFixture; +} + +function rejected(name: string, expectedIssueCode: string, mutate: (draft: MutableFixture) => void): FixtureCase { + const draft = signedStatusDraft(RELEASE_STATUS_YANKED_FIXTURE); + mutate(draft); + return { name, group: "release-status", value: draft, expectedValid: false, expectedIssueCode }; +} + +/** Structure-only cases for `validateReleaseStatus`; signatures are placeholders. */ +export const RELEASE_STATUS_FIXTURES: FixtureCase[] = [ + { name: "valid status without a reason", group: "release-status", value: signedStatusDraft(RELEASE_STATUS_PUBLISHED_FIXTURE), expectedValid: true }, + { name: "valid status with a reason", group: "release-status", value: signedStatusDraft(RELEASE_STATUS_YANKED_FIXTURE), expectedValid: true }, + rejected("rejects an unknown state", "status.unknown_state", (d) => { + d.state = "revoked"; + }), + rejected("rejects a missing state", "required", (d) => { + delete d.state; + }), + rejected("rejects an unknown member", "unknown_field", (d) => { + d.expiresAt = "2027-01-01T00:00:00Z"; + }), + rejected("rejects a missing signature", "required", (d) => { + delete d.signature; + }), + rejected("rejects a zero sequence", "type", (d) => { + d.sequence = 0; + }), + rejected("rejects a fractional sequence", "type", (d) => { + d.sequence = 1.5; + }), + rejected("rejects a numeric-string sequence", "type", (d) => { + d.sequence = "2"; + }), + rejected("rejects an empty reason", "length", (d) => { + d.reason = ""; + }), + rejected("rejects a null reason", "type", (d) => { + d.reason = null; + }), + rejected("rejects a control character in the reason", "format", (d) => { + d.reason = `line one${String.fromCodePoint(0x0a)}line two`; + }), + rejected("rejects a bare-hex releaseDigest", "pattern", (d) => { + d.releaseDigest = "c".repeat(64); + }), + rejected("rejects a timestamp with an offset", "format", (d) => { + d.issuedAt = "2026-10-14T16:00:00+07:00"; + }), + rejected("rejects the release manifest schema version", "unsupported_value", (d) => { + d.schemaVersion = "skillx.release/v1"; + }), + rejected("rejects a key ID outside the registry pattern", "pattern", (d) => { + d.keyId = "Key ID"; + }), + rejected("rejects another signature algorithm", "unsupported_value", (d) => { + d.signatureAlgorithm = "rsa-pss"; + }), +]; diff --git a/packages/contracts/src/fixtures/release-status-v1.json b/packages/contracts/src/fixtures/release-status-v1.json new file mode 100644 index 0000000..3b7a928 --- /dev/null +++ b/packages/contracts/src/fixtures/release-status-v1.json @@ -0,0 +1,344 @@ +{ + "description": "skillx.release-status/v1 signed status vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", + "canonicalization": "same as skillx.release/v1: padded base64 Ed25519 over the UTF-8 canonical JSON of the status without its signature member; an absent reason is omitted, never null", + "consumerRules": [ + "validate the structure; a state outside draft|quarantined|published|yanked is status.unknown_state even when signed", + "require releaseDigest to equal the verified manifest's releaseDigest, else status.digest_mismatch", + "verify the signature with the trusted key for keyId: signature.unknown_key, signature.invalid_key or signature.mismatch", + "with a previously accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict; identical bytes are accepted" + ], + "testKey": { + "keyId": "skillx-test-vector-1", + "seedText": "skillx.release/v1 test vector key; public, never trusted", + "seedHex": "1ec7ec7c27d6e97182ca8a8c0e2f3f2af27f4f7174f70ba70dbfd54f6c12c575", + "publicKeyHex": "8029f998dfaf0fd73e5bdc7762280fbb0a9805af218308d2df9213c64cdfadb2", + "publicKeyBase64": "gCn5mN+vD9c+W9x3YigPuwqYBa8hgwjS35ITxkzfrbI=" + }, + "valid": [ + { + "name": "first status of a release", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": null, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + }, + "signingPayload": "{\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayloadSha256": "25dced168272f8f3b6a271023c8be6d203c8d1d582c42e71df1dc3037ddfa953" + }, + { + "name": "yanked with a reason after the published status", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "signingPayload": "{\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayloadSha256": "3cbe5371237cf16d21fb52fd89f2affa21e9d90249357b3fc919ee70114ed00d" + }, + { + "name": "identical re-fetch of the accepted status", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "signingPayload": "{\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayloadSha256": "3cbe5371237cf16d21fb52fd89f2affa21e9d90249357b3fc919ee70114ed00d" + } + ], + "invalid": [ + { + "name": "state changed after signing", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "expectedCode": "signature.mismatch" + }, + { + "name": "reason changed after signing", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes.", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "expectedCode": "signature.mismatch" + }, + { + "name": "sequence raised after signing", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 3, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "expectedCode": "signature.mismatch" + }, + { + "name": "signed by another key under the trusted key ID", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "owzDAGzu0xnfAu1u3HKVHPUSrVqXvfYs+WI31H2GwG1m9/oVfcy8n81n9jSPYO73AMv65J0r3uPVy9ndqC6+Ag==" + }, + "expectedCode": "signature.mismatch" + }, + { + "name": "unknown key ID", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-unknown-key", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "expectedCode": "signature.unknown_key" + }, + { + "name": "signature over an encoding/json-escaped payload", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "U1Ku+xKBYaDbmqhXe5JGhM2sE3ACt0LnwPWccu5+w2ryqXQLfwzTwkYQInAJ3bc2eVJpeh/xF7PCLHVy38/QBg==" + }, + "expectedCode": "signature.mismatch" + }, + { + "name": "lower sequence than the accepted status (rollback)", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + }, + "expectedCode": "status.rollback" + }, + { + "name": "same sequence with different content (conflict)", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "E82kVVVAu+q9NybBOCl+APmtfyaGY4cDcadZnuH7g3D81DsUJF5IjRbELo6e/bql2ydlQqz1Uu5/ey1Q5oWPAA==" + }, + "expectedCode": "status.conflict" + }, + { + "name": "unknown state, correctly signed", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "revoked", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 3, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "1jC6cC9EV55zB3xahn/kPNzxOnpSt7vqOKG+kp9O2cVw8Z9D4dOi/Qsn196kfUD6HXpF84ZieMytA9roMaBlCg==" + }, + "expectedCode": "status.unknown_state" + }, + { + "name": "status of another release", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "previous": null, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "oWXFM4yuKPQl9nmw5twcD9Xrmu90D2OV5XgWroB9fuWQgMvasUwMSljbD0Z2bnz0M0J36gg8uREFP60qAIiXDA==" + }, + "expectedCode": "status.digest_mismatch" + } + ] +} diff --git a/packages/contracts/src/fixtures/types.ts b/packages/contracts/src/fixtures/types.ts index 6639821..e8280b9 100644 --- a/packages/contracts/src/fixtures/types.ts +++ b/packages/contracts/src/fixtures/types.ts @@ -21,7 +21,8 @@ export type FixtureGroup = | "entitlement" | "listing-import" | "release-publish" - | "release-manifest"; + | "release-manifest" + | "release-status"; /** A syntactically valid digest used by fixtures. */ export const FIXTURE_DIGEST = `sha256:${"a".repeat(64)}`; diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index c3d8e48..e5c31b7 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -12,6 +12,7 @@ export * from "./compatibility"; export * from "./package-manifest"; export * from "./canonical-json"; export * from "./release-manifest"; +export * from "./release-status"; export * from "./collections"; export * from "./ingestion"; export * from "./dto"; diff --git a/packages/contracts/src/package-manifest.test.ts b/packages/contracts/src/package-manifest.test.ts index 2d4d93d..6cb03ca 100644 --- a/packages/contracts/src/package-manifest.test.ts +++ b/packages/contracts/src/package-manifest.test.ts @@ -7,9 +7,11 @@ import { PACKAGE_MANIFEST_FIXTURES, PACKAGE_MANIFEST_FIXTURE, RELEASE_MANIFEST_FIXTURES, + RELEASE_STATUS_FIXTURES, VERIFICATION_EVIDENCE_FIXTURES, } from "./fixtures"; import { validateReleaseManifest } from "./release-manifest"; +import { validateReleaseStatus } from "./release-status"; import { PACKAGE_KINDS, RELEASE_STATES, @@ -31,12 +33,13 @@ const VALIDATOR_BY_GROUP: Record { valid: boolean; i "listing-import": validateListingImport, "release-publish": validateReleasePublish, "release-manifest": validateReleaseManifest, + "release-status": validateReleaseStatus, }; describe("skillx.package/v1 fixtures", () => { it("covers both a valid and a rejected case for every contract group", () => { const groups = new Set(ALL_FIXTURES.map((fixture) => fixture.group)); - expect(groups.size).toBe(7); + expect(groups.size).toBe(8); for (const group of groups) { const cases = ALL_FIXTURES.filter((fixture) => fixture.group === group); expect(cases.some((c) => c.expectedValid), `${group} needs a valid fixture`).toBe(true); @@ -60,13 +63,15 @@ describe("skillx.package/v1 fixtures", () => { expect(ENTITLEMENT_FIXTURES.length).toBeGreaterThan(0); expect(INGESTION_FIXTURES.length).toBeGreaterThan(0); expect(RELEASE_MANIFEST_FIXTURES.length).toBeGreaterThan(0); + expect(RELEASE_STATUS_FIXTURES.length).toBeGreaterThan(0); expect(ALL_FIXTURES.length).toBe( PACKAGE_MANIFEST_FIXTURES.length + VERIFICATION_EVIDENCE_FIXTURES.length + COLLECTION_MEMBER_FIXTURES.length + ENTITLEMENT_FIXTURES.length + INGESTION_FIXTURES.length + - RELEASE_MANIFEST_FIXTURES.length, + RELEASE_MANIFEST_FIXTURES.length + + RELEASE_STATUS_FIXTURES.length, ); }); }); diff --git a/packages/contracts/src/release-manifest.test.ts b/packages/contracts/src/release-manifest.test.ts index 9f3a869..b45d55a 100644 --- a/packages/contracts/src/release-manifest.test.ts +++ b/packages/contracts/src/release-manifest.test.ts @@ -70,7 +70,8 @@ describe("skillx.release/v1 validation", () => { const paths = validateReleaseManifest({}).issues.filter((i) => i.code === "required").map((i) => i.path); expect(paths).toContain("releaseDigest"); expect(paths).toContain("signature"); - expect(paths).toHaveLength(19); + expect(paths).toHaveLength(18); + expect(paths).not.toContain("state"); }); it("rejects unknown nested publisher and provenance fields", () => { @@ -140,7 +141,7 @@ describe("skillx.release/v1 signatures", () => { }); it.each<[string, (m: SkillxReleaseManifest) => void]>([ - ["a changed state", (m) => void (m.state = "yanked")], + ["a changed version", (m) => void (m.version = "1.2.1")], ["a changed file hash", (m) => void (m.files[0]!.sha256 = `${m.files[0]!.sha256.slice(0, 63)}0`)], ["a changed release digest", (m) => void (m.releaseDigest = `sha256:${"e".repeat(64)}`)], ["a changed publisher verification", (m) => void (m.publisher.verifiedBy = "admin")], @@ -198,9 +199,9 @@ describe("skillx.release/v1 signatures", () => { expect(result.issues.map((i) => i.code)).not.toContain(RELEASE_SIGNATURE_CODES.mismatch); }); - it("refuses to sign an invalid manifest", async () => { + it("refuses to sign a manifest that carries a mutable release state", async () => { const { privateKey } = await generateKey(); - const invalid = { ...RELEASE_MANIFEST_FIXTURE, state: "blocked" } as unknown as UnsignedReleaseManifest; + const invalid = { ...RELEASE_MANIFEST_FIXTURE, state: "published" } as unknown as UnsignedReleaseManifest; await expect(signReleaseManifest(invalid, privateKey)).rejects.toBeInstanceOf(ReleaseManifestError); }); diff --git a/packages/contracts/src/release-manifest.ts b/packages/contracts/src/release-manifest.ts index abd3203..10e033b 100644 --- a/packages/contracts/src/release-manifest.ts +++ b/packages/contracts/src/release-manifest.ts @@ -4,34 +4,44 @@ * A release manifest pins one immutable package version to the canonical * `agentkit.skill-tree/v1` tree of its files. `releaseDigest` is the tree * digest and the release identity; `artifactDigest` covers only the bytes of - * the transport archive. The registry signs the manifest with Ed25519 over - * `releaseSigningPayload`: the canonical JSON (see `canonical-json.ts`) of - * every member except `signature`, so `keyId` and `signatureAlgorithm` are - * signed too. The signature is the standard (padded) base64 of the 64-byte - * Ed25519 signature, as in AgentKit's remote registry manifests. + * the transport archive. The manifest is signed once per `releaseDigest` and + * never changes; the mutable release state lives in the separately signed + * `skillx.release-status/v1` document (`release-status.ts`). Signature rules + * are in `release-signature.ts`: Ed25519 over the canonical JSON of every + * member except `signature`. * - * Validators never throw on malformed input. Signing and verification use the - * WebCrypto `Ed25519` algorithm, available in Workers, browsers and Node.js. + * Validators never throw on malformed input. */ import { ERROR_CODES, SkillTreeError, checkCollisions, checkPublisher, isNoisePath, validatePath } from "@skillx/skill-tree/rules"; import { canonicalJsonBytes } from "./canonical-json"; +import { PACKAGE_NAME_PATTERN, RELEASE_CHANNELS, SEMVER_PATTERN, isPackageKind, PACKAGE_KINDS } from "./package-manifest"; +import type { PackageKind, ReleaseChannel } from "./package-manifest"; import { - PACKAGE_NAME_PATTERN, - RELEASE_CHANNELS, - SEMVER_PATTERN, - isPackageKind, - isReleaseState, - PACKAGE_KINDS, - RELEASE_STATES, -} from "./package-manifest"; -import type { PackageKind, ReleaseChannel, ReleaseState } from "./package-manifest"; + RELEASE_SIGNATURE_ALGORITHM, + ReleaseManifestError, + checkFields as checkSchemaFields, + checkSignatureMembers, + isReleaseTimestamp, + signPayload, + verifyPayload, +} from "./release-signature"; +import type { ReleasePublicKeys } from "./release-signature"; import { isPlainObject, issue, validationFail, validationOk } from "./validation"; import type { ValidationIssue, ValidationResult } from "./validation"; import { RELEASE_DIGEST_PATTERN } from "./verification-evidence"; +export { + RELEASE_KEY_ID_PATTERN, + RELEASE_SIGNATURE_ALGORITHM, + RELEASE_SIGNATURE_CODES, + RELEASE_SIGNATURE_PATTERN, + RELEASE_TIMESTAMP_PATTERN, + ReleaseManifestError, +} from "./release-signature"; +export type { ReleasePublicKeys } from "./release-signature"; + export const SKILLX_RELEASE_SCHEMA_VERSION = "skillx.release/v1"; -export const RELEASE_SIGNATURE_ALGORITHM = "ed25519"; /** Mode classes of `agentkit.skill-tree/v1`. */ export const RELEASE_FILE_MODES = ["100644", "100755"] as const; @@ -44,16 +54,10 @@ export type ReleaseFileMode = (typeof RELEASE_FILE_MODES)[number]; export const PUBLISHER_VERIFICATIONS = ["none", "github", "admin"] as const; export type PublisherVerification = (typeof PUBLISHER_VERIFICATIONS)[number]; -/** Same key ID rule as AgentKit remote registry manifests. */ -export const RELEASE_KEY_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{1,63}$/; /** Per-file content hash: bare lowercase hex SHA-256. */ export const FILE_SHA256_PATTERN = /^[a-f0-9]{64}$/; /** A full SHA-1 or SHA-256 git object name in lowercase hex. */ export const SOURCE_COMMIT_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/; -/** RFC 3339 UTC timestamp with whole seconds, e.g. `2026-10-13T04:30:00Z`. */ -export const RELEASE_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; -/** Padded standard base64 of a 64-byte Ed25519 signature. */ -export const RELEASE_SIGNATURE_PATTERN = /^[A-Za-z0-9+/]{86}==$/; const MAX_SOURCE_REPO_LENGTH = 512; /** Tree limits of `agentkit.skill-tree/v1`; equal to `LIMITS` of `@skillx/skill-tree` (guarded by a test). */ @@ -100,10 +104,11 @@ export interface UnsignedReleaseManifest { files: ReleaseFile[]; fileCount: number; treeSize: number; - /** Release state at signing time; a state change is a newly signed manifest for the same digest. */ - state: ReleaseState; publisher: ReleasePublisher; - /** `null` when the release was not packed from a recorded git commit. */ + /** + * `null` when the release was not packed from a recorded git commit. + * Consumers MUST label such a release "provenance unknown". + */ sourceProvenance: ReleaseSourceProvenance | null; /** Paths of `files` the package declares as scripts, sorted like `files`. Never executed by SkillX. */ declaredScripts: string[]; @@ -128,7 +133,6 @@ const MANIFEST_FIELDS = [ "files", "fileCount", "treeSize", - "state", "publisher", "sourceProvenance", "declaredScripts", @@ -140,23 +144,6 @@ const FILE_FIELDS = ["path", "mode", "sha256", "size"] as const; const PUBLISHER_FIELDS = ["handle", "verifiedBy"] as const; const PROVENANCE_FIELDS = ["repo", "commit", "path"] as const; -/** Signature verification failure codes. */ -export const RELEASE_SIGNATURE_CODES = { - unknownKey: "signature.unknown_key", - invalidKey: "signature.invalid_key", - mismatch: "signature.mismatch", -} as const; - -/** Thrown by `signReleaseManifest` for an invalid manifest or key. */ -export class ReleaseManifestError extends Error { - readonly issues: ValidationIssue[]; - constructor(message: string, issues: ValidationIssue[] = []) { - super(issues.length === 0 ? message : `${message}: ${issues.map((i) => `${i.path} ${i.code}`).join(", ")}`); - this.name = "ReleaseManifestError"; - this.issues = issues; - } -} - /** Compares well-formed strings by UTF-8 bytes, which is code point order. */ export function compareUtf8(a: string, b: string): number { const ia = a[Symbol.iterator](); @@ -177,13 +164,7 @@ function oneOf(values: readonly T[], value: unknown): value is } function checkFields(value: Record, allowed: readonly string[], path: string, issues: ValidationIssue[]): void { - const prefix = path === "" ? "" : `${path}.`; - for (const key of Object.keys(value)) { - if (!allowed.includes(key)) issues.push(issue(`${prefix}${key}`, "unknown_field", "is not a skillx.release/v1 field")); - } - for (const key of allowed) { - if (!(key in value)) issues.push(issue(`${prefix}${key}`, "required", "is required")); - } + checkSchemaFields(value, allowed, path, SKILLX_RELEASE_SCHEMA_VERSION, issues); } function treeIssue(path: string, error: SkillTreeError): ValidationIssue { @@ -328,13 +309,6 @@ function validateDeclaredScripts(scripts: unknown, files: ReleaseFile[] | null, }); } -function isReleaseTimestamp(value: unknown): boolean { - if (typeof value !== "string" || !RELEASE_TIMESTAMP_PATTERN.test(value)) return false; - const ms = Date.parse(value); - // Round-tripping rejects calendar overflow such as 2026-02-30. - return Number.isFinite(ms) && new Date(ms).toISOString() === `${value.slice(0, -1)}.000Z`; -} - function collectIssues(input: unknown, requireSignature: boolean): ValidationIssue[] { if (!isPlainObject(input)) return [issue("", "type", "release manifest must be an object")]; const issues: ValidationIssue[] = []; @@ -377,26 +351,13 @@ function collectIssues(input: unknown, requireSignature: boolean): ValidationIss issues.push(issue("treeSize", "mismatch", "must equal the sum of file sizes")); } - if (!isReleaseState(input.state)) { - issues.push(issue("state", "unsupported_value", `must be one of: ${RELEASE_STATES.join(", ")}`)); - } validatePublisher(input.publisher, handle, issues); validateProvenance(input.sourceProvenance, issues); validateDeclaredScripts(input.declaredScripts, files, issues); if (!isReleaseTimestamp(input.createdAt)) { issues.push(issue("createdAt", "format", "must be an RFC 3339 UTC timestamp with whole seconds, e.g. 2026-10-13T04:30:00Z")); } - if (typeof input.keyId !== "string" || !RELEASE_KEY_ID_PATTERN.test(input.keyId)) { - issues.push(issue("keyId", "pattern", `must match ${RELEASE_KEY_ID_PATTERN.source}`)); - } - if (input.signatureAlgorithm !== RELEASE_SIGNATURE_ALGORITHM) { - issues.push(issue("signatureAlgorithm", "unsupported_value", `must equal '${RELEASE_SIGNATURE_ALGORITHM}'`)); - } - if (requireSignature && "signature" in input) { - if (typeof input.signature !== "string" || !RELEASE_SIGNATURE_PATTERN.test(input.signature)) { - issues.push(issue("signature", "pattern", "must be padded base64 of a 64-byte Ed25519 signature")); - } - } + checkSignatureMembers(input, requireSignature, issues); return issues; } @@ -421,22 +382,6 @@ export function releaseSigningPayload(manifest: UnsignedReleaseManifest | Skillx return canonicalJsonBytes(payload); } -function toBase64(bytes: Uint8Array): string { - let binary = ""; - for (const byte of bytes) binary += String.fromCharCode(byte); - return btoa(binary); -} - -function fromBase64(text: string): Uint8Array { - const binary = atob(text); - const out = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i); - return out; -} - -const ED25519 = { name: "Ed25519" } as const; -const ED25519_PUBLIC_KEY_BYTES = 32; - /** * Signs a valid unsigned manifest with an Ed25519 private key. The key ID and * algorithm are part of the signed payload. Throws `ReleaseManifestError` when @@ -448,18 +393,9 @@ export async function signReleaseManifest( ): Promise { const result = validateUnsignedReleaseManifest(manifest); if (!result.valid) throw new ReleaseManifestError("invalid release manifest", result.issues); - let signature: ArrayBuffer; - try { - signature = await crypto.subtle.sign(ED25519, privateKey, releaseSigningPayload(manifest)); - } catch (cause) { - throw new ReleaseManifestError(`cannot sign with the given key: ${cause instanceof Error ? cause.message : String(cause)}`); - } - return { ...manifest, signature: toBase64(new Uint8Array(signature)) }; + return { ...manifest, signature: await signPayload(releaseSigningPayload(manifest), privateKey) }; } -/** A key set: key ID to raw 32-byte Ed25519 public key. */ -export type ReleasePublicKeys = ReadonlyMap; - /** * Validates a signed manifest and verifies its signature against `publicKeys`. * Fails closed: an invalid manifest, unknown key ID, malformed key or bad @@ -469,22 +405,6 @@ export async function verifyReleaseManifest(input: unknown, publicKeys: ReleaseP const structure = validateReleaseManifest(input); if (!structure.valid) return structure; const manifest = input as SkillxReleaseManifest; - const raw = publicKeys.get(manifest.keyId); - if (raw === undefined) { - return validationFail([issue("keyId", RELEASE_SIGNATURE_CODES.unknownKey, `no trusted key ${JSON.stringify(manifest.keyId)}`)]); - } - if (raw.length !== ED25519_PUBLIC_KEY_BYTES) { - return validationFail([issue("keyId", RELEASE_SIGNATURE_CODES.invalidKey, `key ${JSON.stringify(manifest.keyId)} is not 32 bytes`)]); - } - let verified = false; - try { - const key = await crypto.subtle.importKey("raw", new Uint8Array(raw), ED25519, false, ["verify"]); - verified = await crypto.subtle.verify(ED25519, key, fromBase64(manifest.signature), releaseSigningPayload(manifest)); - } catch (cause) { - const detail = cause instanceof Error ? cause.message : String(cause); - return validationFail([issue("keyId", RELEASE_SIGNATURE_CODES.invalidKey, `key ${JSON.stringify(manifest.keyId)} is unusable: ${detail}`)]); - } - return verified - ? validationOk() - : validationFail([issue("signature", RELEASE_SIGNATURE_CODES.mismatch, "signature does not verify for the canonical payload")]); + const failure = await verifyPayload(releaseSigningPayload(manifest), manifest.signature, manifest.keyId, publicKeys); + return failure === null ? validationOk() : validationFail([failure]); } diff --git a/packages/contracts/src/release-signature.ts b/packages/contracts/src/release-signature.ts new file mode 100644 index 0000000..496b806 --- /dev/null +++ b/packages/contracts/src/release-signature.ts @@ -0,0 +1,137 @@ +/** + * Signature, key and timestamp rules shared by the signed SkillX release + * documents (`skillx.release/v1` and `skillx.release-status/v1`). + * + * A document is signed with Ed25519 over the UTF-8 canonical JSON (see + * `canonical-json.ts`) of every member except `signature`, so `keyId` and + * `signatureAlgorithm` are signed too. The signature is the standard (padded) + * base64 of the 64-byte Ed25519 signature, as in AgentKit's remote registry + * manifests. WebCrypto `Ed25519` works in Workers, browsers and Node.js. + */ + +import { issue } from "./validation"; +import type { ValidationIssue } from "./validation"; + +export const RELEASE_SIGNATURE_ALGORITHM = "ed25519"; +/** Same key ID rule as AgentKit remote registry manifests. */ +export const RELEASE_KEY_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{1,63}$/; +/** RFC 3339 UTC timestamp with whole seconds, e.g. `2026-10-13T04:30:00Z`. */ +export const RELEASE_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; +/** Padded standard base64 of a 64-byte Ed25519 signature. */ +export const RELEASE_SIGNATURE_PATTERN = /^[A-Za-z0-9+/]{86}==$/; + +/** Signature verification failure codes. */ +export const RELEASE_SIGNATURE_CODES = { + unknownKey: "signature.unknown_key", + invalidKey: "signature.invalid_key", + mismatch: "signature.mismatch", +} as const; + +/** A key set: key ID to raw 32-byte Ed25519 public key. */ +export type ReleasePublicKeys = ReadonlyMap; + +const ED25519 = { name: "Ed25519" } as const; +const ED25519_PUBLIC_KEY_BYTES = 32; + +/** Reports unknown and missing members of one object level of a `schema` document. */ +export function checkFields( + value: Record, + required: readonly string[], + path: string, + schema: string, + issues: ValidationIssue[], + optional: readonly string[] = [], +): void { + const prefix = path === "" ? "" : `${path}.`; + for (const key of Object.keys(value)) { + if (!required.includes(key) && !optional.includes(key)) { + issues.push(issue(`${prefix}${key}`, "unknown_field", `is not a ${schema} field`)); + } + } + for (const key of required) { + if (!(key in value)) issues.push(issue(`${prefix}${key}`, "required", "is required")); + } +} + +export function isReleaseTimestamp(value: unknown): boolean { + if (typeof value !== "string" || !RELEASE_TIMESTAMP_PATTERN.test(value)) return false; + const ms = Date.parse(value); + // Round-tripping rejects calendar overflow such as 2026-02-30. + return Number.isFinite(ms) && new Date(ms).toISOString() === `${value.slice(0, -1)}.000Z`; +} + +/** Pushes the issues of the signed `keyId`, `signatureAlgorithm` and (when required) `signature` members. */ +export function checkSignatureMembers(input: Record, requireSignature: boolean, issues: ValidationIssue[]): void { + if (typeof input.keyId !== "string" || !RELEASE_KEY_ID_PATTERN.test(input.keyId)) { + issues.push(issue("keyId", "pattern", `must match ${RELEASE_KEY_ID_PATTERN.source}`)); + } + if (input.signatureAlgorithm !== RELEASE_SIGNATURE_ALGORITHM) { + issues.push(issue("signatureAlgorithm", "unsupported_value", `must equal '${RELEASE_SIGNATURE_ALGORITHM}'`)); + } + if (requireSignature && "signature" in input) { + if (typeof input.signature !== "string" || !RELEASE_SIGNATURE_PATTERN.test(input.signature)) { + issues.push(issue("signature", "pattern", "must be padded base64 of a 64-byte Ed25519 signature")); + } + } +} + +function toBase64(bytes: Uint8Array): string { + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary); +} + +function fromBase64(text: string): Uint8Array { + const binary = atob(text); + const out = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i); + return out; +} + +/** Thrown by `signReleaseManifest` and `signReleaseStatus` for an invalid document or key. */ +export class ReleaseManifestError extends Error { + readonly issues: ValidationIssue[]; + constructor(message: string, issues: ValidationIssue[] = []) { + super(issues.length === 0 ? message : `${message}: ${issues.map((i) => `${i.path} ${i.code}`).join(", ")}`); + this.name = "ReleaseManifestError"; + this.issues = issues; + } +} + +/** + * Signs `payload` and returns the padded base64 signature. Throws + * `ReleaseManifestError` when the key cannot sign with Ed25519. + */ +export async function signPayload(payload: Uint8Array, privateKey: CryptoKey): Promise { + try { + return toBase64(new Uint8Array(await crypto.subtle.sign(ED25519, privateKey, payload))); + } catch (cause) { + throw new ReleaseManifestError(`cannot sign with the given key: ${cause instanceof Error ? cause.message : String(cause)}`); + } +} + +/** + * Verifies a structurally valid signature over `payload`. Returns `null` on + * success, otherwise the single failure issue. Never throws. + */ +export async function verifyPayload( + payload: Uint8Array, + signature: string, + keyId: string, + publicKeys: ReleasePublicKeys, +): Promise { + const raw = publicKeys.get(keyId); + if (raw === undefined) return issue("keyId", RELEASE_SIGNATURE_CODES.unknownKey, `no trusted key ${JSON.stringify(keyId)}`); + if (raw.length !== ED25519_PUBLIC_KEY_BYTES) { + return issue("keyId", RELEASE_SIGNATURE_CODES.invalidKey, `key ${JSON.stringify(keyId)} is not 32 bytes`); + } + let verified = false; + try { + const key = await crypto.subtle.importKey("raw", new Uint8Array(raw), ED25519, false, ["verify"]); + verified = await crypto.subtle.verify(ED25519, key, fromBase64(signature), payload); + } catch (cause) { + const detail = cause instanceof Error ? cause.message : String(cause); + return issue("keyId", RELEASE_SIGNATURE_CODES.invalidKey, `key ${JSON.stringify(keyId)} is unusable: ${detail}`); + } + return verified ? null : issue("signature", RELEASE_SIGNATURE_CODES.mismatch, "signature does not verify for the canonical payload"); +} diff --git a/packages/contracts/src/release-status-vectors.test.ts b/packages/contracts/src/release-status-vectors.test.ts new file mode 100644 index 0000000..465fe1e --- /dev/null +++ b/packages/contracts/src/release-status-vectors.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, it } from "vitest"; +import { + RELEASE_FIXTURE_KEY_ID, + RELEASE_MANIFEST_FIXTURE, + RELEASE_MANIFEST_MINIMAL_FIXTURE, + RELEASE_STATUS_FIXTURE_REASON, + RELEASE_STATUS_PUBLISHED_FIXTURE, + RELEASE_STATUS_YANKED_FIXTURE, +} from "./fixtures"; +import vectorFile from "./fixtures/release-status-v1.json"; +import { RELEASE_SIGNATURE_CODES } from "./release-manifest"; +import { signPayload } from "./release-signature"; +import { RELEASE_STATUS_CODES, releaseStatusSigningPayload, signReleaseStatus, verifyReleaseStatus } from "./release-status"; +import type { SkillxReleaseStatus, UnsignedReleaseStatus } from "./release-status"; +import { + OTHER_VECTOR_KEY_SEED_TEXT, + VECTOR_KEY_SEED_TEXT, + base64, + clone, + decoder, + describeVectorKey, + fromBase64, + goJsonStyle, + hex, + sha256, + vectorKey, + writeVectorFile, + writeVectorsRequested, +} from "./test-support/vector-key"; +import type { VectorKey, VectorKeyDescription } from "./test-support/vector-key"; + +/** + * Golden cross-language vectors for `skillx.release-status/v1`, exported for + * AgentKit (`fixtures/release-status-v1.json`). Same public test key as the + * manifest vectors. Each vector names the release digest the consumer expects + * and the status it accepted before (`previous`, or `null`). + * + * Regenerate after an intentional contract change with + * `SKILLX_WRITE_RELEASE_VECTORS=1 pnpm vitest run packages/contracts/src`. + */ + +interface StatusVector { + name: string; + expectedReleaseDigest: string; + previous: SkillxReleaseStatus | null; + status: SkillxReleaseStatus; +} + +interface StatusVectors { + description: string; + canonicalization: string; + consumerRules: string[]; + testKey: VectorKeyDescription; + valid: Array; + invalid: Array; +} + +/** Signs any status bytes, including ones `signReleaseStatus` refuses (e.g. an unknown state). */ +async function signRaw(status: Record, key: VectorKey): Promise { + const signature = await signPayload(releaseStatusSigningPayload(status as unknown as UnsignedReleaseStatus), key.privateKey); + return { ...(status as unknown as UnsignedReleaseStatus), signature }; +} + +async function buildVectors(): Promise { + const key = await vectorKey(VECTOR_KEY_SEED_TEXT); + const other = await vectorKey(OTHER_VECTOR_KEY_SEED_TEXT); + const digest = RELEASE_MANIFEST_FIXTURE.releaseDigest; + + const published = await signReleaseStatus(RELEASE_STATUS_PUBLISHED_FIXTURE, key.privateKey); + const yanked = await signReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE, key.privateKey); + + const valid: StatusVectors["valid"] = []; + for (const [name, status, previous] of [ + ["first status of a release", published, null], + ["yanked with a reason after the published status", yanked, published], + ["identical re-fetch of the accepted status", yanked, yanked], + ] as const) { + const payload = releaseStatusSigningPayload(status); + valid.push({ + name, + expectedReleaseDigest: digest, + previous, + status, + signingPayload: decoder.decode(payload), + signingPayloadSha256: hex(await sha256(payload)), + }); + } + + const stateChanged = clone(yanked); + stateChanged.state = "published"; + const reasonChanged = clone(yanked); + reasonChanged.reason = `${RELEASE_STATUS_FIXTURE_REASON}.`; + const sequenceRaised = clone(yanked); + sequenceRaised.sequence = 3; + const otherKey = await signReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE, other.privateKey); + const unknownKey = clone(yanked); + unknownKey.keyId = "skillx-unknown-key"; + const goStyle = clone(yanked); + goStyle.signature = base64( + new Uint8Array( + await crypto.subtle.sign({ name: "Ed25519" }, key.privateKey, goJsonStyle(releaseStatusSigningPayload(RELEASE_STATUS_YANKED_FIXTURE))), + ), + ); + const conflicting = await signReleaseStatus( + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, sequence: 2, issuedAt: "2026-10-14T09:00:00Z" }, + key.privateKey, + ); + const unknownState = await signRaw({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, state: "revoked", sequence: 3 }, key); + const otherRelease = await signReleaseStatus( + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest }, + key.privateKey, + ); + + const mismatch = RELEASE_SIGNATURE_CODES.mismatch; + const invalid = (name: string, status: SkillxReleaseStatus, previous: SkillxReleaseStatus | null, expectedCode: string) => ({ + name, + expectedReleaseDigest: digest, + previous, + status, + expectedCode, + }); + + return { + description: + "skillx.release-status/v1 signed status vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", + canonicalization: + "same as skillx.release/v1: padded base64 Ed25519 over the UTF-8 canonical JSON of the status without its signature member; an absent reason is omitted, never null", + consumerRules: [ + "validate the structure; a state outside draft|quarantined|published|yanked is status.unknown_state even when signed", + "require releaseDigest to equal the verified manifest's releaseDigest, else status.digest_mismatch", + "verify the signature with the trusted key for keyId: signature.unknown_key, signature.invalid_key or signature.mismatch", + "with a previously accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict; identical bytes are accepted", + ], + testKey: describeVectorKey(RELEASE_FIXTURE_KEY_ID, VECTOR_KEY_SEED_TEXT, key), + valid, + invalid: [ + invalid("state changed after signing", stateChanged, published, mismatch), + invalid("reason changed after signing", reasonChanged, published, mismatch), + invalid("sequence raised after signing", sequenceRaised, yanked, mismatch), + invalid("signed by another key under the trusted key ID", otherKey, published, mismatch), + invalid("unknown key ID", unknownKey, published, RELEASE_SIGNATURE_CODES.unknownKey), + invalid("signature over an encoding/json-escaped payload", goStyle, published, mismatch), + invalid("lower sequence than the accepted status (rollback)", published, yanked, RELEASE_STATUS_CODES.rollback), + invalid("same sequence with different content (conflict)", conflicting, yanked, RELEASE_STATUS_CODES.conflict), + invalid("unknown state, correctly signed", unknownState, yanked, RELEASE_STATUS_CODES.unknownState), + invalid("status of another release", otherRelease, null, RELEASE_STATUS_CODES.digestMismatch), + ], + }; +} + +const committed = vectorFile as unknown as StatusVectors; + +function trustedKeys(): Map { + return new Map([[committed.testKey.keyId, fromBase64(committed.testKey.publicKeyBase64)]]); +} + +describe("skillx.release-status/v1 cross-language vectors", () => { + it.runIf(writeVectorsRequested)("writes the vector file", async () => { + await writeVectorFile("release-status-v1.json", await buildVectors()); + }); + + it.skipIf(writeVectorsRequested)("match the committed vector file byte for byte in content", async () => { + expect(committed).toEqual(await buildVectors()); + }); + + it.skipIf(writeVectorsRequested)("accept every valid vector", async () => { + expect(committed.valid.length).toBe(3); + for (const vector of committed.valid) { + const result = await verifyReleaseStatus(vector.status, trustedKeys(), { + releaseDigest: vector.expectedReleaseDigest, + previous: vector.previous, + }); + expect(result, vector.name).toEqual({ valid: true, issues: [] }); + expect(decoder.decode(releaseStatusSigningPayload(vector.status))).toBe(vector.signingPayload); + } + }); + + it.skipIf(writeVectorsRequested)("reject every invalid vector with its code", async () => { + const codes = new Set(); + for (const vector of committed.invalid) { + const result = await verifyReleaseStatus(vector.status, trustedKeys(), { + releaseDigest: vector.expectedReleaseDigest, + previous: vector.previous, + }); + expect(result.issues.map((i) => i.code), vector.name).toEqual([vector.expectedCode]); + codes.add(vector.expectedCode); + } + expect([...codes].sort()).toEqual( + [...Object.values(RELEASE_STATUS_CODES), RELEASE_SIGNATURE_CODES.mismatch, RELEASE_SIGNATURE_CODES.unknownKey].sort(), + ); + }); + + it.skipIf(writeVectorsRequested)("signs the reason with raw non-ASCII and HTML characters", () => { + const yanked = committed.valid[1]!; + expect(yanked.signingPayload).toContain(RELEASE_STATUS_FIXTURE_REASON); + expect(yanked.signingPayload).not.toContain("\\u00"); + }); +}); diff --git a/packages/contracts/src/release-status.test.ts b/packages/contracts/src/release-status.test.ts new file mode 100644 index 0000000..3558289 --- /dev/null +++ b/packages/contracts/src/release-status.test.ts @@ -0,0 +1,168 @@ +import { describe, expect, it } from "vitest"; +import { + RELEASE_FIXTURE_KEY_ID, + RELEASE_MANIFEST_FIXTURE, + RELEASE_MANIFEST_MINIMAL_FIXTURE, + RELEASE_STATUS_PUBLISHED_FIXTURE, + RELEASE_STATUS_YANKED_FIXTURE, +} from "./fixtures"; +import { RELEASE_SIGNATURE_CODES, ReleaseManifestError } from "./release-manifest"; +import { + RELEASE_STATUS_CODES, + RELEASE_STATUS_REASON_MAX_LENGTH, + releaseStatusSigningPayload, + signReleaseStatus, + validateReleaseStatus, + validateUnsignedReleaseStatus, + verifyReleaseStatus, +} from "./release-status"; +import type { SkillxReleaseStatus, UnsignedReleaseStatus } from "./release-status"; + +const decoder = new TextDecoder(); +const DIGEST = RELEASE_MANIFEST_FIXTURE.releaseDigest; + +async function generateKey(): Promise<{ privateKey: CryptoKey; publicKey: Uint8Array }> { + const pair = (await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"])) as CryptoKeyPair; + return { privateKey: pair.privateKey, publicKey: new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey)) }; +} + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +async function signedPair() { + const { privateKey, publicKey } = await generateKey(); + const keys = new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]]); + const published = await signReleaseStatus(RELEASE_STATUS_PUBLISHED_FIXTURE, privateKey); + const yanked = await signReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE, privateKey); + return { privateKey, keys, published, yanked }; +} + +const codes = (result: { issues: { code: string }[] }) => result.issues.map((i) => i.code); + +describe("skillx.release-status/v1 validation", () => { + it("accepts both unsigned fixtures before signing", () => { + expect(validateUnsignedReleaseStatus(RELEASE_STATUS_PUBLISHED_FIXTURE)).toEqual({ valid: true, issues: [] }); + expect(validateUnsignedReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE)).toEqual({ valid: true, issues: [] }); + }); + + it("rejects non-object input without throwing", () => { + for (const input of [null, undefined, "published", [], 1]) expect(validateReleaseStatus(input).valid).toBe(false); + }); + + it("reports every required member of an empty object, but not the optional reason", () => { + const paths = validateReleaseStatus({}).issues.filter((i) => i.code === "required").map((i) => i.path); + expect(paths.sort()).toEqual( + ["issuedAt", "keyId", "releaseDigest", "schemaVersion", "sequence", "signature", "signatureAlgorithm", "state"].sort(), + ); + }); + + it("rejects an undefined reason member instead of failing to canonicalize", () => { + const status = { ...RELEASE_STATUS_PUBLISHED_FIXTURE, reason: undefined }; + expect(codes(validateUnsignedReleaseStatus(status))).toEqual(["type"]); + }); + + it("bounds the reason length", () => { + const at = (length: number) => ({ ...RELEASE_STATUS_YANKED_FIXTURE, reason: "x".repeat(length) }); + expect(validateUnsignedReleaseStatus(at(RELEASE_STATUS_REASON_MAX_LENGTH)).valid).toBe(true); + expect(codes(validateUnsignedReleaseStatus(at(RELEASE_STATUS_REASON_MAX_LENGTH + 1)))).toEqual(["length"]); + }); + + it("rejects a lone surrogate in the reason", () => { + const status = { ...RELEASE_STATUS_YANKED_FIXTURE, reason: `bad ${String.fromCharCode(0xd800)}` }; + expect(codes(validateUnsignedReleaseStatus(status))).toEqual(["format"]); + }); + + it("rejects an unsafe sequence", () => { + const status = { ...RELEASE_STATUS_PUBLISHED_FIXTURE, sequence: Number.MAX_SAFE_INTEGER + 1 }; + expect(codes(validateUnsignedReleaseStatus(status))).toEqual(["type"]); + }); +}); + +describe("skillx.release-status/v1 signing payload", () => { + it("is byte-stable across member order and omits an absent reason", () => { + const reordered = Object.fromEntries(Object.entries(RELEASE_STATUS_PUBLISHED_FIXTURE).reverse()) as unknown as UnsignedReleaseStatus; + expect(releaseStatusSigningPayload(reordered)).toEqual(releaseStatusSigningPayload(RELEASE_STATUS_PUBLISHED_FIXTURE)); + const text = decoder.decode(releaseStatusSigningPayload(RELEASE_STATUS_PUBLISHED_FIXTURE)); + expect(text).toBe( + `{"issuedAt":"2026-10-13T04:31:00Z","keyId":"skillx-test-vector-1","releaseDigest":"${DIGEST}","schemaVersion":"skillx.release-status/v1","sequence":1,"signatureAlgorithm":"ed25519","state":"published"}`, + ); + }); +}); + +describe("skillx.release-status/v1 verification", () => { + it("verifies a first status and a later one against the accepted status", async () => { + const { keys, published, yanked } = await signedPair(); + expect(await verifyReleaseStatus(published, keys, { releaseDigest: DIGEST })).toEqual({ valid: true, issues: [] }); + expect(await verifyReleaseStatus(yanked, keys, { releaseDigest: DIGEST, previous: published })).toEqual({ valid: true, issues: [] }); + expect(await verifyReleaseStatus(yanked, keys, { releaseDigest: DIGEST, previous: yanked })).toEqual({ valid: true, issues: [] }); + }); + + it.each<[string, (s: SkillxReleaseStatus) => void]>([ + ["a changed state", (s) => void (s.state = "published")], + ["a changed reason", (s) => void (s.reason = "other")], + ["a removed reason", (s) => void delete s.reason], + ["a raised sequence", (s) => void (s.sequence = 9)], + ["a changed issue time", (s) => void (s.issuedAt = "2026-10-15T00:00:00Z")], + ])("rejects %s", async (_name, tamper) => { + const { keys, yanked } = await signedPair(); + const tampered = clone(yanked); + tamper(tampered); + expect(codes(await verifyReleaseStatus(tampered, keys, { releaseDigest: DIGEST }))).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + }); + + it("rejects a status signed by another key under the trusted key ID", async () => { + const { yanked } = await signedPair(); + const trusted = await generateKey(); + const result = await verifyReleaseStatus(yanked, new Map([[RELEASE_FIXTURE_KEY_ID, trusted.publicKey]]), { releaseDigest: DIGEST }); + expect(codes(result)).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + }); + + it("rejects an unknown key ID", async () => { + const { yanked, keys } = await signedPair(); + const result = await verifyReleaseStatus(yanked, new Map([["skillx-other-key", keys.get(RELEASE_FIXTURE_KEY_ID)!]]), { + releaseDigest: DIGEST, + }); + expect(codes(result)).toEqual([RELEASE_SIGNATURE_CODES.unknownKey]); + }); + + it("rejects a lower sequence than the accepted status", async () => { + const { keys, published, yanked } = await signedPair(); + const result = await verifyReleaseStatus(published, keys, { releaseDigest: DIGEST, previous: yanked }); + expect(codes(result)).toEqual([RELEASE_STATUS_CODES.rollback]); + }); + + it("rejects a different status with the accepted sequence", async () => { + const { privateKey, keys, yanked } = await signedPair(); + const rival = await signReleaseStatus({ ...RELEASE_STATUS_YANKED_FIXTURE, state: "published" }, privateKey); + const result = await verifyReleaseStatus(rival, keys, { releaseDigest: DIGEST, previous: yanked }); + expect(codes(result)).toEqual([RELEASE_STATUS_CODES.conflict]); + }); + + it("rejects a status of another release", async () => { + const { keys, published } = await signedPair(); + const result = await verifyReleaseStatus(published, keys, { releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest }); + expect(codes(result)).toEqual([RELEASE_STATUS_CODES.digestMismatch]); + }); + + it("fails closed on an unknown state before checking the signature", async () => { + const { keys, published } = await signedPair(); + const unknown = { ...clone(published), state: "revoked" }; + expect(codes(await verifyReleaseStatus(unknown, keys, { releaseDigest: DIGEST }))).toEqual([RELEASE_STATUS_CODES.unknownState]); + }); + + it("throws when the caller passes a previous status of another release", async () => { + const { privateKey, keys, yanked } = await signedPair(); + const foreign = await signReleaseStatus( + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest }, + privateKey, + ); + await expect(verifyReleaseStatus(yanked, keys, { releaseDigest: DIGEST, previous: foreign })).rejects.toBeInstanceOf(ReleaseManifestError); + }); + + it("refuses to sign an unknown state", async () => { + const { privateKey } = await generateKey(); + const invalid = { ...RELEASE_STATUS_PUBLISHED_FIXTURE, state: "revoked" } as unknown as UnsignedReleaseStatus; + await expect(signReleaseStatus(invalid, privateKey)).rejects.toBeInstanceOf(ReleaseManifestError); + }); +}); diff --git a/packages/contracts/src/release-status.ts b/packages/contracts/src/release-status.ts new file mode 100644 index 0000000..c36d42f --- /dev/null +++ b/packages/contracts/src/release-status.ts @@ -0,0 +1,185 @@ +/** + * `skillx.release-status/v1` signed release status contract. + * + * The `skillx.release/v1` manifest is signed once per `releaseDigest` and + * never changes. The mutable lifecycle state of that release (published, + * yanked, quarantined, ...) is a separate document, signed with the same + * canonical-bytes and Ed25519 rules (`release-signature.ts`). Each new status + * for a release carries a strictly higher `sequence`, so a consumer that keeps + * the last status it accepted can reject a replayed older one: + * + * - a status whose `sequence` is lower than the last accepted one is a + * rollback (`status.rollback`); + * - a status with the same `sequence` but different signed bytes is a + * conflict (`status.conflict`); an identical re-fetch is accepted; + * - a `state` outside `RELEASE_STATES` fails closed (`status.unknown_state`), + * even when correctly signed, so an older consumer never guesses. + * + * Validators never throw on malformed input. + */ + +import { canonicalJsonBytes } from "./canonical-json"; +import { RELEASE_STATES, isReleaseState } from "./package-manifest"; +import type { ReleaseState } from "./package-manifest"; +import { + RELEASE_SIGNATURE_ALGORITHM, + ReleaseManifestError, + checkFields, + checkSignatureMembers, + isReleaseTimestamp, + signPayload, + verifyPayload, +} from "./release-signature"; +import type { ReleasePublicKeys } from "./release-signature"; +import { isPlainObject, issue, validationFail, validationOk } from "./validation"; +import type { ValidationIssue, ValidationResult } from "./validation"; +import { RELEASE_DIGEST_PATTERN } from "./verification-evidence"; + +export const SKILLX_RELEASE_STATUS_SCHEMA_VERSION = "skillx.release-status/v1"; +/** Maximum `reason` length in UTF-16 code units. */ +export const RELEASE_STATUS_REASON_MAX_LENGTH = 512; + +/** Status-specific failure codes, in addition to the signature codes. */ +export const RELEASE_STATUS_CODES = { + unknownState: "status.unknown_state", + digestMismatch: "status.digest_mismatch", + rollback: "status.rollback", + conflict: "status.conflict", +} as const; + +export interface UnsignedReleaseStatus { + schemaVersion: typeof SKILLX_RELEASE_STATUS_SCHEMA_VERSION; + /** The `releaseDigest` of the `skillx.release/v1` manifest this status describes. */ + releaseDigest: string; + state: ReleaseState; + /** Optional human-readable explanation, e.g. why a release was yanked. Omit the member when there is none. */ + reason?: string; + issuedAt: string; + /** Strictly increasing per `releaseDigest`, starting at 1. */ + sequence: number; + keyId: string; + signatureAlgorithm: typeof RELEASE_SIGNATURE_ALGORITHM; +} + +export interface SkillxReleaseStatus extends UnsignedReleaseStatus { + signature: string; +} + +export interface ReleaseStatusVerifyOptions { + /** The `releaseDigest` of the verified release manifest the status must describe. */ + releaseDigest: string; + /** + * The status this consumer last accepted for the same release, if any. It + * must be a status this function accepted before; an invalid one throws. + */ + previous?: SkillxReleaseStatus | null; +} + +const STATUS_FIELDS = ["schemaVersion", "releaseDigest", "state", "issuedAt", "sequence", "keyId", "signatureAlgorithm"] as const; +const OPTIONAL_STATUS_FIELDS = ["reason"] as const; + +function validateReason(reason: unknown, issues: ValidationIssue[]): void { + if (typeof reason !== "string") { + issues.push(issue("reason", "type", "must be a string when present")); + } else if (reason.length === 0 || reason.length > RELEASE_STATUS_REASON_MAX_LENGTH) { + issues.push(issue("reason", "length", `must be 1 to ${RELEASE_STATUS_REASON_MAX_LENGTH} characters`)); + } else if (/\p{Cc}|\p{Cs}/u.test(reason)) { + issues.push(issue("reason", "format", "must not contain control characters or lone surrogates")); + } +} + +function collectIssues(input: unknown, requireSignature: boolean): ValidationIssue[] { + if (!isPlainObject(input)) return [issue("", "type", "release status must be an object")]; + const issues: ValidationIssue[] = []; + const required = requireSignature ? [...STATUS_FIELDS, "signature"] : STATUS_FIELDS; + checkFields(input, required, "", SKILLX_RELEASE_STATUS_SCHEMA_VERSION, issues, OPTIONAL_STATUS_FIELDS); + + if (input.schemaVersion !== SKILLX_RELEASE_STATUS_SCHEMA_VERSION) { + issues.push(issue("schemaVersion", "unsupported_value", `must equal '${SKILLX_RELEASE_STATUS_SCHEMA_VERSION}'`)); + } + if (typeof input.releaseDigest !== "string" || !RELEASE_DIGEST_PATTERN.test(input.releaseDigest)) { + issues.push(issue("releaseDigest", "pattern", "must match sha256:<64 lowercase hex>")); + } + if ("state" in input && !isReleaseState(input.state)) { + issues.push(issue("state", RELEASE_STATUS_CODES.unknownState, `must be one of: ${RELEASE_STATES.join(", ")}`)); + } + if ("reason" in input) validateReason(input.reason, issues); + if (!isReleaseTimestamp(input.issuedAt)) { + issues.push(issue("issuedAt", "format", "must be an RFC 3339 UTC timestamp with whole seconds, e.g. 2026-10-13T04:30:00Z")); + } + if (!Number.isSafeInteger(input.sequence) || (input.sequence as number) < 1) { + issues.push(issue("sequence", "type", "must be a positive safe integer")); + } + checkSignatureMembers(input, requireSignature, issues); + return issues; +} + +/** Validates a signed `skillx.release-status/v1` document (structure only, not the signature). */ +export function validateReleaseStatus(input: unknown): ValidationResult { + const issues = collectIssues(input, true); + return issues.length === 0 ? validationOk() : validationFail(issues); +} + +/** Validates a status before it is signed: every field except `signature`, which must be absent. */ +export function validateUnsignedReleaseStatus(input: unknown): ValidationResult { + const issues = collectIssues(input, false); + return issues.length === 0 ? validationOk() : validationFail(issues); +} + +/** The exact bytes a status signature covers: the canonical JSON of the status without `signature`. */ +export function releaseStatusSigningPayload(status: UnsignedReleaseStatus | SkillxReleaseStatus): Uint8Array { + const { signature: _signature, ...payload } = status as Partial; + return canonicalJsonBytes(payload); +} + +/** Signs a valid unsigned status. Throws `ReleaseManifestError` when the status or key is invalid. */ +export async function signReleaseStatus(status: UnsignedReleaseStatus, privateKey: CryptoKey): Promise { + const result = validateUnsignedReleaseStatus(status); + if (!result.valid) throw new ReleaseManifestError("invalid release status", result.issues); + return { ...status, signature: await signPayload(releaseStatusSigningPayload(status), privateKey) }; +} + +function sameBytes(a: Uint8Array, b: Uint8Array): boolean { + return a.length === b.length && a.every((byte, i) => byte === b[i]); +} + +/** + * Validates a signed status, binds it to `options.releaseDigest`, verifies its + * signature against `publicKeys` and, when `options.previous` is given, + * enforces the monotonic sequence. Fails closed on every malformed input, an + * unknown state, an unknown or malformed key, a bad signature, a rollback or + * a conflict; it never throws for `input`. + */ +export async function verifyReleaseStatus( + input: unknown, + publicKeys: ReleasePublicKeys, + options: ReleaseStatusVerifyOptions, +): Promise { + const structure = validateReleaseStatus(input); + if (!structure.valid) return structure; + const status = input as SkillxReleaseStatus; + if (status.releaseDigest !== options.releaseDigest) { + return validationFail([issue("releaseDigest", RELEASE_STATUS_CODES.digestMismatch, "does not describe the expected release")]); + } + const payload = releaseStatusSigningPayload(status); + const failure = await verifyPayload(payload, status.signature, status.keyId, publicKeys); + if (failure !== null) return validationFail([failure]); + + const previous = options.previous; + if (previous === undefined || previous === null) return validationOk(); + const previousResult = validateReleaseStatus(previous); + if (!previousResult.valid || previous.releaseDigest !== options.releaseDigest) { + throw new ReleaseManifestError("previous status is not an accepted status of this release", previousResult.issues); + } + if (status.sequence < previous.sequence) { + return validationFail([ + issue("sequence", RELEASE_STATUS_CODES.rollback, `is lower than the last accepted sequence ${previous.sequence}`), + ]); + } + if (status.sequence === previous.sequence && !sameBytes(payload, releaseStatusSigningPayload(previous))) { + return validationFail([ + issue("sequence", RELEASE_STATUS_CODES.conflict, `differs from the accepted status with the same sequence ${previous.sequence}`), + ]); + } + return validationOk(); +} diff --git a/packages/contracts/src/release-vectors.test.ts b/packages/contracts/src/release-vectors.test.ts index b5eea46..9e57e2c 100644 --- a/packages/contracts/src/release-vectors.test.ts +++ b/packages/contracts/src/release-vectors.test.ts @@ -14,7 +14,24 @@ import { validateReleaseManifest, verifyReleaseManifest, } from "./release-manifest"; -import type { SkillxReleaseManifest, UnsignedReleaseManifest } from "./release-manifest"; +import type { SkillxReleaseManifest } from "./release-manifest"; +import { + OTHER_VECTOR_KEY_SEED_TEXT, + VECTOR_KEY_SEED_TEXT, + base64, + clone, + decoder, + describeVectorKey, + encoder, + fromBase64, + goJsonStyle, + hex, + sha256, + vectorKey, + writeVectorFile, + writeVectorsRequested, +} from "./test-support/vector-key"; +import type { VectorKeyDescription } from "./test-support/vector-key"; /** * Golden cross-language vectors for `skillx.release/v1`, exported for @@ -23,23 +40,13 @@ import type { SkillxReleaseManifest, UnsignedReleaseManifest } from "./release-m * worthless outside tests. * * Regenerate after an intentional contract change with - * `SKILLX_WRITE_RELEASE_VECTORS=1 pnpm vitest run packages/contracts/src/release-vectors.test.ts`. + * `SKILLX_WRITE_RELEASE_VECTORS=1 pnpm vitest run packages/contracts/src`. */ -const KEY_SEED_TEXT = "skillx.release/v1 test vector key; public, never trusted"; -const OTHER_KEY_SEED_TEXT = "skillx.release/v1 other test vector key; public, never trusted"; -const PKCS8_ED25519_PREFIX = [0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20]; - -interface VectorKey { - privateKey: CryptoKey; - seed: Uint8Array; - publicKey: Uint8Array; -} - interface ReleaseVectors { description: string; canonicalization: string; - testKey: { keyId: string; seedText: string; seedHex: string; publicKeyHex: string; publicKeyBase64: string }; + testKey: VectorKeyDescription; valid: Array<{ name: string; manifest: SkillxReleaseManifest; @@ -51,48 +58,19 @@ interface ReleaseVectors { invalidManifest: Array<{ name: string; manifest: unknown; expectedCode: string }>; } -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); -const hex = (bytes: Uint8Array) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); -const base64 = (bytes: Uint8Array) => btoa(String.fromCharCode(...bytes)); -const fromBase64 = (text: string) => Uint8Array.from(atob(text), (c) => c.charCodeAt(0)); -const fromBase64Url = (text: string) => fromBase64(text.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text.length / 4) * 4, "=")); - -async function sha256(data: Uint8Array): Promise { - return new Uint8Array(await crypto.subtle.digest("SHA-256", data)); -} - -async function vectorKey(seedText: string): Promise { - const seed = await sha256(encoder.encode(seedText)); - const pkcs8 = new Uint8Array([...PKCS8_ED25519_PREFIX, ...seed]); - const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, true, ["sign"]); - const jwk = await crypto.subtle.exportKey("jwk", privateKey); - return { privateKey, seed, publicKey: fromBase64Url(jwk.x ?? "") }; -} - -function clone(value: T): T { - return JSON.parse(JSON.stringify(value)) as T; -} - function contentsBase64(name: keyof typeof RELEASE_FIXTURE_CONTENTS): Record { return Object.fromEntries(Object.entries(RELEASE_FIXTURE_CONTENTS[name]).map(([path, text]) => [path, base64(encoder.encode(text))])); } -/** The payload a Go implementation would get from encoding/json.Marshal-style escaping. */ -function goJsonStylePayload(manifest: UnsignedReleaseManifest): Uint8Array { - const text = decoder.decode(releaseSigningPayload(manifest)).replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028"); - return encoder.encode(text); -} - async function buildVectors(): Promise { - const key = await vectorKey(KEY_SEED_TEXT); - const other = await vectorKey(OTHER_KEY_SEED_TEXT); + const key = await vectorKey(VECTOR_KEY_SEED_TEXT); + const other = await vectorKey(OTHER_VECTOR_KEY_SEED_TEXT); const valid: ReleaseVectors["valid"] = []; const signedFull = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, key.privateKey); for (const [name, base, tree] of [ - ["published multi-file release", RELEASE_MANIFEST_FIXTURE, "full"], - ["quarantined minimal prerelease", RELEASE_MANIFEST_MINIMAL_FIXTURE, "minimal"], + ["publisher-verified multi-file release", RELEASE_MANIFEST_FIXTURE, "full"], + ["unverified minimal prerelease with unknown provenance", RELEASE_MANIFEST_MINIMAL_FIXTURE, "minimal"], ] as const) { const manifest = await signReleaseManifest(base, key.privateKey); const payload = releaseSigningPayload(manifest); @@ -106,8 +84,8 @@ async function buildVectors(): Promise { } const mismatch = RELEASE_SIGNATURE_CODES.mismatch; - const stateChanged = clone(signedFull); - stateChanged.state = "yanked"; + const versionChanged = clone(signedFull); + versionChanged.version = "1.2.1"; const flipped = clone(signedFull); const signatureBytes = fromBase64(flipped.signature); signatureBytes[0] = signatureBytes[0]! ^ 0x01; @@ -117,7 +95,9 @@ async function buildVectors(): Promise { unknownKey.keyId = "skillx-unknown-key"; const goStyle = clone(signedFull); goStyle.signature = base64( - new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key.privateKey, goJsonStylePayload(RELEASE_MANIFEST_FIXTURE))), + new Uint8Array( + await crypto.subtle.sign({ name: "Ed25519" }, key.privateKey, goJsonStyle(releaseSigningPayload(RELEASE_MANIFEST_FIXTURE))), + ), ); return { @@ -125,16 +105,10 @@ async function buildVectors(): Promise { "skillx.release/v1 signed manifest vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", canonicalization: "signature = padded base64 Ed25519 over UTF-8 canonical JSON (RFC 8785 subset: members sorted by UTF-16 code units, no whitespace, non-negative safe integers only, only quote/backslash/C0 controls escaped, everything else raw UTF-8) of the manifest without its signature member; see packages/contracts/src/canonical-json.ts", - testKey: { - keyId: RELEASE_FIXTURE_KEY_ID, - seedText: KEY_SEED_TEXT, - seedHex: hex(key.seed), - publicKeyHex: hex(key.publicKey), - publicKeyBase64: base64(key.publicKey), - }, + testKey: describeVectorKey(RELEASE_FIXTURE_KEY_ID, VECTOR_KEY_SEED_TEXT, key), valid, invalidSignature: [ - { name: "state changed after signing", manifest: stateChanged, expectedCode: mismatch }, + { name: "version changed after signing", manifest: versionChanged, expectedCode: mismatch }, { name: "flipped signature byte", manifest: flipped, expectedCode: mismatch }, { name: "signed by another key under the trusted key ID", manifest: otherKey, expectedCode: mismatch }, { name: "unknown key ID", manifest: unknownKey, expectedCode: RELEASE_SIGNATURE_CODES.unknownKey }, @@ -149,32 +123,28 @@ async function buildVectors(): Promise { } const committed = vectorFile as unknown as ReleaseVectors; -const writeRequested = (globalThis as { process?: { env: Record } }).process?.env - .SKILLX_WRITE_RELEASE_VECTORS === "1"; describe("skillx.release/v1 cross-language vectors", () => { - it.runIf(writeRequested)("writes the vector file", async () => { - // A computed specifier keeps Node typings out of the Worker typecheck that also covers this file. - const fsModule = "node:fs"; - const fs = (await import(/* @vite-ignore */ fsModule)) as { writeFileSync(path: URL, data: string): void }; - fs.writeFileSync(new URL("./fixtures/release-manifest-v1.json", import.meta.url), `${JSON.stringify(await buildVectors(), null, 2)}\n`); + it.runIf(writeVectorsRequested)("writes the vector file", async () => { + await writeVectorFile("release-manifest-v1.json", await buildVectors()); }); - it.skipIf(writeRequested)("match the committed vector file byte for byte in content", async () => { + it.skipIf(writeVectorsRequested)("match the committed vector file byte for byte in content", async () => { expect(committed).toEqual(await buildVectors()); }); - it.skipIf(writeRequested)("verify every valid vector with the published test key", async () => { + it.skipIf(writeVectorsRequested)("verify every valid vector with the published test key", async () => { const keys = new Map([[committed.testKey.keyId, fromBase64(committed.testKey.publicKeyBase64)]]); expect(committed.valid.length).toBe(2); for (const vector of committed.valid) { expect(await verifyReleaseManifest(vector.manifest, keys), vector.name).toEqual({ valid: true, issues: [] }); expect(decoder.decode(releaseSigningPayload(vector.manifest))).toBe(vector.signingPayload); expect(Object.keys(vector.fileContentsBase64)).toEqual(vector.manifest.files.map((f) => f.path)); + expect(vector.manifest).not.toHaveProperty("state"); } }); - it.skipIf(writeRequested)("hash every vector file to its manifest entry", async () => { + it.skipIf(writeVectorsRequested)("hash every vector file to its manifest entry", async () => { for (const vector of committed.valid) { for (const file of vector.manifest.files) { const bytes = fromBase64(vector.fileContentsBase64[file.path]!); @@ -184,7 +154,7 @@ describe("skillx.release/v1 cross-language vectors", () => { } }); - it.skipIf(writeRequested)("reject every invalid-signature vector with its code", async () => { + it.skipIf(writeVectorsRequested)("reject every invalid-signature vector with its code", async () => { const keys = new Map([[committed.testKey.keyId, fromBase64(committed.testKey.publicKeyBase64)]]); for (const vector of committed.invalidSignature) { expect(validateReleaseManifest(vector.manifest).valid, vector.name).toBe(true); @@ -193,7 +163,7 @@ describe("skillx.release/v1 cross-language vectors", () => { } }); - it.skipIf(writeRequested)("reject every invalid-manifest vector with its code", () => { + it.skipIf(writeVectorsRequested)("reject every invalid-manifest vector with its code", () => { expect(committed.invalidManifest.length).toBeGreaterThan(10); for (const vector of committed.invalidManifest) { const result = validateReleaseManifest(vector.manifest); diff --git a/packages/contracts/src/test-support/vector-key.ts b/packages/contracts/src/test-support/vector-key.ts new file mode 100644 index 0000000..80d8a49 --- /dev/null +++ b/packages/contracts/src/test-support/vector-key.ts @@ -0,0 +1,76 @@ +/** + * Test-only helpers for the signed cross-language release vectors. The keys + * are derived from public seed texts, so they are reproducible and worthless + * outside tests. Not exported from the package entry point. + */ + +export const VECTOR_KEY_SEED_TEXT = "skillx.release/v1 test vector key; public, never trusted"; +export const OTHER_VECTOR_KEY_SEED_TEXT = "skillx.release/v1 other test vector key; public, never trusted"; + +/** DER prefix of a PKCS #8 Ed25519 private key; the 32-byte seed follows. */ +const PKCS8_ED25519_PREFIX = [0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20]; + +export interface VectorKey { + privateKey: CryptoKey; + seed: Uint8Array; + publicKey: Uint8Array; +} + +export interface VectorKeyDescription { + keyId: string; + seedText: string; + seedHex: string; + publicKeyHex: string; + publicKeyBase64: string; +} + +export const encoder = new TextEncoder(); +export const decoder = new TextDecoder(); +export const hex = (bytes: Uint8Array): string => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); +export const base64 = (bytes: Uint8Array): string => btoa(String.fromCharCode(...bytes)); +export const fromBase64 = (text: string): Uint8Array => Uint8Array.from(atob(text), (c) => c.charCodeAt(0)); +const fromBase64Url = (text: string) => fromBase64(text.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text.length / 4) * 4, "=")); + +export async function sha256(data: Uint8Array): Promise { + return new Uint8Array(await crypto.subtle.digest("SHA-256", data)); +} + +/** Derives the Ed25519 key whose seed is the SHA-256 of `seedText`. */ +export async function vectorKey(seedText: string): Promise { + const seed = await sha256(encoder.encode(seedText)); + const pkcs8 = new Uint8Array([...PKCS8_ED25519_PREFIX, ...seed]); + const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, true, ["sign"]); + const jwk = await crypto.subtle.exportKey("jwk", privateKey); + return { privateKey, seed, publicKey: fromBase64Url(jwk.x ?? "") }; +} + +export function describeVectorKey(keyId: string, seedText: string, key: VectorKey): VectorKeyDescription { + return { keyId, seedText, seedHex: hex(key.seed), publicKeyHex: hex(key.publicKey), publicKeyBase64: base64(key.publicKey) }; +} + +export function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +/** The payload a Go implementation would get from encoding/json.Marshal-style HTML and line-separator escaping. */ +export function goJsonStyle(payload: Uint8Array): Uint8Array { + const text = decoder + .decode(payload) + .replace(/&/g, "\\u0026") + .replace(//g, "\\u003e") + .replace(new RegExp(String.fromCodePoint(0x2028), "g"), "\\u2028"); + return encoder.encode(text); +} + +/** Writes `data` as pretty JSON next to this package's fixtures (only when regenerating vectors). */ +export async function writeVectorFile(fileName: string, data: unknown): Promise { + // A computed specifier keeps Node typings out of the Worker typecheck that also covers this file. + const fsModule = "node:fs"; + const fs = (await import(/* @vite-ignore */ fsModule)) as { writeFileSync(path: URL, data: string): void }; + fs.writeFileSync(new URL(`../fixtures/${fileName}`, import.meta.url), `${JSON.stringify(data, null, 2)}\n`); +} + +/** True when the vector files should be regenerated instead of compared. */ +export const writeVectorsRequested = + (globalThis as { process?: { env: Record } }).process?.env.SKILLX_WRITE_RELEASE_VECTORS === "1"; From 2cb973b95064af3f830cd802674db17e9effc027 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 12:08:22 +0700 Subject: [PATCH 10/14] feat(contracts): bind release status to one signed manifest The same tree can be released under several identities or versions, so a status keyed only by releaseDigest would apply to every sibling release. Add a required manifestDigest: the sha256 of the manifest signing payload, the exact bytes the manifest signature covers. Verification now rejects a status for another manifest with status.manifest_mismatch, and rollback and conflict checks track the sequence per manifest digest. Vectors list the signed manifests with their digests and cover one tree under two versions and a missing or wrong manifest digest. Document that the registry stores the full signed status document, so reason needs no database column. --- docs/api-reference.md | 35 +- .../src/fixtures/release-status-fixtures.ts | 13 + .../src/fixtures/release-status-v1.json | 328 ++++++++++++++++-- packages/contracts/src/release-manifest.ts | 12 + .../src/release-status-vectors.test.ts | 101 ++++-- packages/contracts/src/release-status.test.ts | 104 ++++-- packages/contracts/src/release-status.ts | 49 ++- 7 files changed, 536 insertions(+), 106 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 532b6c4..64e2a2a 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -794,27 +794,44 @@ base64). Source: `packages/contracts/src/release-status.ts`. |--------|------|------| | `schemaVersion` | string | `skillx.release-status/v1` | | `releaseDigest` | string | `releaseDigest` of the manifest the status describes | +| `manifestDigest` | string | `sha256:<64 lowercase hex>` of that manifest's signing payload (below); names exactly one release | | `state` | string | `draft`, `quarantined`, `published`, `yanked` | | `reason` | string, optional | 1 to 512 characters, no control characters; omit the member when there is none (never `null`) | | `issuedAt` | string | RFC 3339 UTC with whole seconds | -| `sequence` | integer | strictly increasing per release, starting at 1 | +| `sequence` | integer | strictly increasing per `manifestDigest`, starting at 1 | | `keyId` | string | `^[a-z0-9][a-z0-9._-]{1,63}$` | | `signatureAlgorithm` | string | `ed25519` | | `signature` | string | padded standard base64 of the 64-byte Ed25519 signature | +**Manifest digest.** `manifestDigest` is `sha256:` followed by the lowercase +hex SHA-256 of the manifest signing payload: the UTF-8 canonical JSON of the +signed `skillx.release/v1` manifest with only `signature` removed, i.e. the +exact bytes the manifest signature covers (`releaseManifestDigest`). It covers +`keyId` and `signatureAlgorithm` but not the signature encoding. The same tree +(`releaseDigest`) released under another identity or version has a different +manifest digest, so a status never applies to a sibling release. + A consumer checks, in this order, and fails closed: -1. structure; a `state` outside the list above is `status.unknown_state`, even when correctly signed; +1. structure; a missing member is `required`; a `state` outside the list above + is `status.unknown_state`, even when correctly signed; 2. `releaseDigest` equals the verified manifest's `releaseDigest`, else `status.digest_mismatch`; -3. the signature (`signature.unknown_key`, `signature.invalid_key`, `signature.mismatch`); -4. against the last status it accepted for the release: a lower `sequence` is - `status.rollback`; the same `sequence` with different signed bytes is - `status.conflict`; identical bytes are accepted. +3. `manifestDigest` equals the verified manifest's manifest digest, else `status.manifest_mismatch`; +4. the signature (`signature.unknown_key`, `signature.invalid_key`, `signature.mismatch`); +5. against the last status it accepted for the same `manifestDigest`: a lower + `sequence` is `status.rollback`; the same `sequence` with different signed + bytes is `status.conflict`; identical bytes are accepted. + +Storage (pinned read API work): the registry stores and serves the full signed +status document as issued, so `reason` needs no database column; D1 keeps only +the state, sequence and issue time used to issue the next status. Cross-language vectors: `packages/contracts/src/fixtures/release-status-v1.json` -(valid statuses with their signing payload and the previously accepted status; -tampered, wrong-key, unknown-key, Go-escaped, rollback, conflict, unknown-state -and other-release cases). Same public test key as the manifest vectors. +(the signed manifests with their manifest digests, including one tree under two +versions; valid statuses with their signing payload and the previously accepted +status; tampered, wrong-key, unknown-key, Go-escaped, rollback, conflict, +unknown-state, other-tree, other-version, missing and wrong `manifestDigest` +cases). Same public test key as the manifest vectors. ### Database columns (migration `0011_release_tree_digest`) diff --git a/packages/contracts/src/fixtures/release-status-fixtures.ts b/packages/contracts/src/fixtures/release-status-fixtures.ts index 4d1b0dd..2926453 100644 --- a/packages/contracts/src/fixtures/release-status-fixtures.ts +++ b/packages/contracts/src/fixtures/release-status-fixtures.ts @@ -14,10 +14,16 @@ import type { FixtureCase, MutableFixture } from "./types"; */ export const RELEASE_STATUS_FIXTURE_REASON = `Yanked: unsafe script & caf${String.fromCodePoint(0xe9)} notes`; +/** `releaseManifestDigest(RELEASE_MANIFEST_FIXTURE)`; recomputed by a test. */ +export const RELEASE_FIXTURE_MANIFEST_DIGEST = "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc"; +/** `releaseManifestDigest(RELEASE_MANIFEST_MINIMAL_FIXTURE)`; recomputed by a test. */ +export const RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST = "sha256:5979df5f8e7c400ac422183440040742bb34274bca37c9347d100948871e3596"; + /** The first status of the full fixture release. */ export const RELEASE_STATUS_PUBLISHED_FIXTURE: UnsignedReleaseStatus = { schemaVersion: "skillx.release-status/v1", releaseDigest: RELEASE_MANIFEST_FIXTURE.releaseDigest, + manifestDigest: RELEASE_FIXTURE_MANIFEST_DIGEST, state: "published", issuedAt: "2026-10-13T04:31:00Z", sequence: 1, @@ -29,6 +35,7 @@ export const RELEASE_STATUS_PUBLISHED_FIXTURE: UnsignedReleaseStatus = { export const RELEASE_STATUS_YANKED_FIXTURE: UnsignedReleaseStatus = { schemaVersion: "skillx.release-status/v1", releaseDigest: RELEASE_MANIFEST_FIXTURE.releaseDigest, + manifestDigest: RELEASE_FIXTURE_MANIFEST_DIGEST, state: "yanked", reason: RELEASE_STATUS_FIXTURE_REASON, issuedAt: "2026-10-14T09:00:00Z", @@ -86,6 +93,12 @@ export const RELEASE_STATUS_FIXTURES: FixtureCase[] = [ rejected("rejects a bare-hex releaseDigest", "pattern", (d) => { d.releaseDigest = "c".repeat(64); }), + rejected("rejects a missing manifestDigest", "required", (d) => { + delete d.manifestDigest; + }), + rejected("rejects an uppercase manifestDigest", "pattern", (d) => { + d.manifestDigest = String(d.manifestDigest).toUpperCase(); + }), rejected("rejects a timestamp with an offset", "format", (d) => { d.issuedAt = "2026-10-14T16:00:00+07:00"; }), diff --git a/packages/contracts/src/fixtures/release-status-v1.json b/packages/contracts/src/fixtures/release-status-v1.json index 3b7a928..202cb53 100644 --- a/packages/contracts/src/fixtures/release-status-v1.json +++ b/packages/contracts/src/fixtures/release-status-v1.json @@ -1,11 +1,13 @@ { "description": "skillx.release-status/v1 signed status vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", "canonicalization": "same as skillx.release/v1: padded base64 Ed25519 over the UTF-8 canonical JSON of the status without its signature member; an absent reason is omitted, never null", + "manifestDigestRule": "manifestDigest = 'sha256:' + lowercase hex SHA-256 of the manifest signing payload: the UTF-8 canonical JSON of the skillx.release/v1 manifest without its signature member (the exact bytes its signature covers)", "consumerRules": [ - "validate the structure; a state outside draft|quarantined|published|yanked is status.unknown_state even when signed", + "validate the structure; a state outside draft|quarantined|published|yanked is status.unknown_state even when signed; a missing member is required", "require releaseDigest to equal the verified manifest's releaseDigest, else status.digest_mismatch", + "require manifestDigest to equal the verified manifest's manifest digest, else status.manifest_mismatch", "verify the signature with the trusted key for keyId: signature.unknown_key, signature.invalid_key or signature.mismatch", - "with a previously accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict; identical bytes are accepted" + "per manifestDigest, against the last accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict; identical bytes are accepted" ], "testKey": { "keyId": "skillx-test-vector-1", @@ -14,331 +16,591 @@ "publicKeyHex": "8029f998dfaf0fd73e5bdc7762280fbb0a9805af218308d2df9213c64cdfadb2", "publicKeyBase64": "gCn5mN+vD9c+W9x3YigPuwqYBa8hgwjS35ITxkzfrbI=" }, + "manifests": [ + { + "name": "1.2.0 of the multi-file tree", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==" + }, + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc" + }, + { + "name": "1.2.1 of the same tree", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.1", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 10240, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "luJjP0RMcubnl6QZf3boxrHDen2iAchsvxQ19zqAFkE79xqSO2DpevYIEDGmeL5Qw0LDOrbGxuc/uqHvWBqXCw==" + }, + "manifestDigest": "sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9" + }, + { + "name": "minimal release", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@solo.dev/tiny_skill", + "version": "0.1.0-beta.1", + "channel": "beta", + "kind": "skill", + "releaseDigest": "sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544", + "artifactDigest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "artifactSize": 1536, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d", + "size": 44 + } + ], + "fileCount": 1, + "treeSize": 44, + "publisher": { + "handle": "solo.dev", + "verifiedBy": "none" + }, + "sourceProvenance": null, + "declaredScripts": [], + "createdAt": "2026-10-13T05:00:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "tEUDJBptJIcz6nVFciKaixGz3bUvTe5ZAVI6cYBFtWsK75DU3bTqSfauO8Tf2Te85zfHA3IbY0FOYPviKWS/Dw==" + }, + "manifestDigest": "sha256:5979df5f8e7c400ac422183440040742bb34274bca37c9347d100948871e3596" + } + ], "valid": [ { "name": "first status of a release", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": null, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 1, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" }, - "signingPayload": "{\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", - "signingPayloadSha256": "25dced168272f8f3b6a271023c8be6d203c8d1d582c42e71df1dc3037ddfa953" + "signingPayload": "{\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayloadSha256": "2cbea4f5fef221d394cb81997f40c7b5a68eff98035644789207181583c9933c" }, { "name": "yanked with a reason after the published status", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 1, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, - "signingPayload": "{\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", - "signingPayloadSha256": "3cbe5371237cf16d21fb52fd89f2affa21e9d90249357b3fc919ee70114ed00d" + "signingPayload": "{\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayloadSha256": "23aaf64b36bb65d12bc11e2d59807050107077087dd9e57d9f9d1c2b48ba31a4" }, { "name": "identical re-fetch of the accepted status", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, - "signingPayload": "{\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", - "signingPayloadSha256": "3cbe5371237cf16d21fb52fd89f2affa21e9d90249357b3fc919ee70114ed00d" + "signingPayload": "{\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayloadSha256": "23aaf64b36bb65d12bc11e2d59807050107077087dd9e57d9f9d1c2b48ba31a4" + }, + { + "name": "first status of another version of the same tree, tracked separately", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9", + "previous": null, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "XMbxiXFkPsciIx6e082vX7myNF6+EwSgoO/uuokUpZk9cgAEMWD3XlOXHZrZxHCCU2eCjWDAfGat/ZoUkqwnCA==" + }, + "signingPayload": "{\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayloadSha256": "6507e4a7925d17dff14749afdc06f714ec034da5e0347cca5d65e843d7ac7498" } ], "invalid": [ { "name": "state changed after signing", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 1, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, "expectedCode": "signature.mismatch" }, { "name": "reason changed after signing", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 1, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes.", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, "expectedCode": "signature.mismatch" }, { "name": "sequence raised after signing", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 3, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, "expectedCode": "signature.mismatch" }, { "name": "signed by another key under the trusted key ID", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 1, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "owzDAGzu0xnfAu1u3HKVHPUSrVqXvfYs+WI31H2GwG1m9/oVfcy8n81n9jSPYO73AMv65J0r3uPVy9ndqC6+Ag==" + "signature": "mNutFrw3UlBsoihubIpXLd0uDzsFL53ZMDslc+tkeqg4ygt6104NEn2hRN6OEKqpUG1OLQsXN9cP47mYUUb7Bw==" }, "expectedCode": "signature.mismatch" }, { "name": "unknown key ID", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 1, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-unknown-key", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, "expectedCode": "signature.unknown_key" }, { "name": "signature over an encoding/json-escaped payload", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 1, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "U1Ku+xKBYaDbmqhXe5JGhM2sE3ACt0LnwPWccu5+w2ryqXQLfwzTwkYQInAJ3bc2eVJpeh/xF7PCLHVy38/QBg==" + "signature": "OngrStlS3ffBcROhE5/LaiSASqqoR8tDqFNyhZ9/pmJzndXqIwwTTWrXJ58zLwYikmCKLuY7bq55DW9MA01mAw==" }, "expectedCode": "signature.mismatch" }, { "name": "lower sequence than the accepted status (rollback)", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 1, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" }, "expectedCode": "status.rollback" }, { "name": "same sequence with different content (conflict)", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "published", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "E82kVVVAu+q9NybBOCl+APmtfyaGY4cDcadZnuH7g3D81DsUJF5IjRbELo6e/bql2ydlQqz1Uu5/ey1Q5oWPAA==" + "signature": "1QO+vcKl/Py+KlkINlH1YCFvvlj4C7ZobdaKN8zqONW9vix64W20AfJEH5go7F1glDT46pwN5d+L6JuKIYbSDA==" }, "expectedCode": "status.conflict" }, { "name": "unknown state, correctly signed", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "yanked", "reason": "Yanked: unsafe script & café notes", "issuedAt": "2026-10-14T09:00:00Z", "sequence": 2, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "BAV+dOJyEQB8OSwBpZ8ons8Td9i+sbNwNlWC0ml+Kjr8feSRlNRUeOQi7pAlmg72AgWLdmHejNxo5GCFOmFeCA==" + "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" }, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "state": "revoked", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 3, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "1jC6cC9EV55zB3xahn/kPNzxOnpSt7vqOKG+kp9O2cVw8Z9D4dOi/Qsn196kfUD6HXpF84ZieMytA9roMaBlCg==" + "signature": "/Fwqm9JZtFZRn/+2juzQKxsmctcsjVkaARtDulsalIS7aKwRSMty/tQUJ0UriJfyL0Uo+y3T4+u3duE/zeqSBg==" }, "expectedCode": "status.unknown_state" }, { - "name": "status of another release", + "name": "status of another release tree", "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", "previous": null, "status": { "schemaVersion": "skillx.release-status/v1", "releaseDigest": "sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544", + "manifestDigest": "sha256:5979df5f8e7c400ac422183440040742bb34274bca37c9347d100948871e3596", "state": "published", "issuedAt": "2026-10-13T04:31:00Z", "sequence": 1, "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "oWXFM4yuKPQl9nmw5twcD9Xrmu90D2OV5XgWroB9fuWQgMvasUwMSljbD0Z2bnz0M0J36gg8uREFP60qAIiXDA==" + "signature": "ZOZz9Gji6upxt6uIOH5RBfOHBnDhI7SXCEcXyqLhDzO0a+XkM9O+DecmW++ATHoYEB1KW8pt+4nbdVqmWi9qBw==" }, "expectedCode": "status.digest_mismatch" + }, + { + "name": "status of another version of the same tree", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", + "previous": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" + }, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9", + "state": "yanked", + "reason": "Yanked: unsafe script & café notes", + "issuedAt": "2026-10-14T09:00:00Z", + "sequence": 2, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "rKAqPl+D+SOgMhSvX8FKHZ2VmUvAxilg744OKej/gGrpyvuHf8yYHui4sQ5+GBCrjpHKUG3Z6284vAwlVDW7BA==" + }, + "expectedCode": "status.manifest_mismatch" + }, + { + "name": "missing manifestDigest, correctly signed", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", + "previous": null, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" + }, + "expectedCode": "required" + }, + { + "name": "wrong manifestDigest, correctly signed", + "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", + "previous": null, + "status": { + "schemaVersion": "skillx.release-status/v1", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "manifestDigest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "state": "published", + "issuedAt": "2026-10-13T04:31:00Z", + "sequence": 1, + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "pD8ZqKLe0csnXK+uN46nAzwEb9K0O5qnpK1qiLS9cXOKEnx9jYPu4FCBi2nbiz/MwTUfPzQ+QRQjv/znoSz0DA==" + }, + "expectedCode": "status.manifest_mismatch" } ] } diff --git a/packages/contracts/src/release-manifest.ts b/packages/contracts/src/release-manifest.ts index 10e033b..0bacfec 100644 --- a/packages/contracts/src/release-manifest.ts +++ b/packages/contracts/src/release-manifest.ts @@ -382,6 +382,18 @@ export function releaseSigningPayload(manifest: UnsignedReleaseManifest | Skillx return canonicalJsonBytes(payload); } +/** + * The manifest digest that binds a `skillx.release-status/v1` document to one + * release: `sha256:<64 lowercase hex>` of `releaseSigningPayload(manifest)`, + * the exact bytes the manifest signature covers (so it includes `keyId` and + * `signatureAlgorithm` but not `signature`). Two releases of the same tree + * under different identities or versions have different manifest digests. + */ +export async function releaseManifestDigest(manifest: UnsignedReleaseManifest | SkillxReleaseManifest): Promise { + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", releaseSigningPayload(manifest))); + return `sha256:${Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("")}`; +} + /** * Signs a valid unsigned manifest with an Ed25519 private key. The key ID and * algorithm are part of the signed payload. Throws `ReleaseManifestError` when diff --git a/packages/contracts/src/release-status-vectors.test.ts b/packages/contracts/src/release-status-vectors.test.ts index 465fe1e..6878b84 100644 --- a/packages/contracts/src/release-status-vectors.test.ts +++ b/packages/contracts/src/release-status-vectors.test.ts @@ -1,14 +1,17 @@ import { describe, expect, it } from "vitest"; import { RELEASE_FIXTURE_KEY_ID, + RELEASE_FIXTURE_MANIFEST_DIGEST, RELEASE_MANIFEST_FIXTURE, RELEASE_MANIFEST_MINIMAL_FIXTURE, + RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST, RELEASE_STATUS_FIXTURE_REASON, RELEASE_STATUS_PUBLISHED_FIXTURE, RELEASE_STATUS_YANKED_FIXTURE, } from "./fixtures"; import vectorFile from "./fixtures/release-status-v1.json"; -import { RELEASE_SIGNATURE_CODES } from "./release-manifest"; +import { RELEASE_SIGNATURE_CODES, releaseManifestDigest, signReleaseManifest, verifyReleaseManifest } from "./release-manifest"; +import type { SkillxReleaseManifest } from "./release-manifest"; import { signPayload } from "./release-signature"; import { RELEASE_STATUS_CODES, releaseStatusSigningPayload, signReleaseStatus, verifyReleaseStatus } from "./release-status"; import type { SkillxReleaseStatus, UnsignedReleaseStatus } from "./release-status"; @@ -32,8 +35,10 @@ import type { VectorKey, VectorKeyDescription } from "./test-support/vector-key" /** * Golden cross-language vectors for `skillx.release-status/v1`, exported for * AgentKit (`fixtures/release-status-v1.json`). Same public test key as the - * manifest vectors. Each vector names the release digest the consumer expects - * and the status it accepted before (`previous`, or `null`). + * manifest vectors. `manifests` lists the signed manifests the statuses refer + * to with their manifest digests; each vector names the release and manifest + * digests the consumer expects and the status it accepted before for that + * manifest (`previous`, or `null`). * * Regenerate after an intentional contract change with * `SKILLX_WRITE_RELEASE_VECTORS=1 pnpm vitest run packages/contracts/src`. @@ -42,6 +47,7 @@ import type { VectorKey, VectorKeyDescription } from "./test-support/vector-key" interface StatusVector { name: string; expectedReleaseDigest: string; + expectedManifestDigest: string; previous: SkillxReleaseStatus | null; status: SkillxReleaseStatus; } @@ -49,8 +55,10 @@ interface StatusVector { interface StatusVectors { description: string; canonicalization: string; + manifestDigestRule: string; consumerRules: string[]; testKey: VectorKeyDescription; + manifests: Array<{ name: string; manifest: SkillxReleaseManifest; manifestDigest: string }>; valid: Array; invalid: Array; } @@ -66,19 +74,37 @@ async function buildVectors(): Promise { const other = await vectorKey(OTHER_VECTOR_KEY_SEED_TEXT); const digest = RELEASE_MANIFEST_FIXTURE.releaseDigest; + // The same tree released as 1.2.0 and 1.2.1: one releaseDigest, two manifest digests. + const manifests: StatusVectors["manifests"] = []; + for (const [name, base] of [ + ["1.2.0 of the multi-file tree", RELEASE_MANIFEST_FIXTURE], + ["1.2.1 of the same tree", { ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }], + ["minimal release", RELEASE_MANIFEST_MINIMAL_FIXTURE], + ] as const) { + const manifest = await signReleaseManifest(base, key.privateKey); + manifests.push({ name, manifest, manifestDigest: await releaseManifestDigest(manifest) }); + } + const nextVersionDigest = manifests[1]!.manifestDigest; + const published = await signReleaseStatus(RELEASE_STATUS_PUBLISHED_FIXTURE, key.privateKey); const yanked = await signReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE, key.privateKey); + const nextVersionPublished = await signReleaseStatus( + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: nextVersionDigest }, + key.privateKey, + ); const valid: StatusVectors["valid"] = []; - for (const [name, status, previous] of [ - ["first status of a release", published, null], - ["yanked with a reason after the published status", yanked, published], - ["identical re-fetch of the accepted status", yanked, yanked], + for (const [name, status, previous, manifestDigest] of [ + ["first status of a release", published, null, RELEASE_FIXTURE_MANIFEST_DIGEST], + ["yanked with a reason after the published status", yanked, published, RELEASE_FIXTURE_MANIFEST_DIGEST], + ["identical re-fetch of the accepted status", yanked, yanked, RELEASE_FIXTURE_MANIFEST_DIGEST], + ["first status of another version of the same tree, tracked separately", nextVersionPublished, null, nextVersionDigest], ] as const) { const payload = releaseStatusSigningPayload(status); valid.push({ name, expectedReleaseDigest: digest, + expectedManifestDigest: manifestDigest, previous, status, signingPayload: decoder.decode(payload), @@ -107,7 +133,18 @@ async function buildVectors(): Promise { ); const unknownState = await signRaw({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, state: "revoked", sequence: 3 }, key); const otherRelease = await signReleaseStatus( - { ...RELEASE_STATUS_PUBLISHED_FIXTURE, releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest }, + { + ...RELEASE_STATUS_PUBLISHED_FIXTURE, + releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest, + manifestDigest: RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST, + }, + key.privateKey, + ); + const nextVersionYanked = await signReleaseStatus({ ...RELEASE_STATUS_YANKED_FIXTURE, manifestDigest: nextVersionDigest }, key.privateKey); + const { manifestDigest: _omitted, ...withoutManifestDigest } = RELEASE_STATUS_PUBLISHED_FIXTURE; + const missingManifestDigest = await signRaw(withoutManifestDigest, key); + const wrongManifestDigest = await signReleaseStatus( + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: `sha256:${"e".repeat(64)}` }, key.privateKey, ); @@ -115,6 +152,7 @@ async function buildVectors(): Promise { const invalid = (name: string, status: SkillxReleaseStatus, previous: SkillxReleaseStatus | null, expectedCode: string) => ({ name, expectedReleaseDigest: digest, + expectedManifestDigest: RELEASE_FIXTURE_MANIFEST_DIGEST, previous, status, expectedCode, @@ -125,13 +163,17 @@ async function buildVectors(): Promise { "skillx.release-status/v1 signed status vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", canonicalization: "same as skillx.release/v1: padded base64 Ed25519 over the UTF-8 canonical JSON of the status without its signature member; an absent reason is omitted, never null", + manifestDigestRule: + "manifestDigest = 'sha256:' + lowercase hex SHA-256 of the manifest signing payload: the UTF-8 canonical JSON of the skillx.release/v1 manifest without its signature member (the exact bytes its signature covers)", consumerRules: [ - "validate the structure; a state outside draft|quarantined|published|yanked is status.unknown_state even when signed", + "validate the structure; a state outside draft|quarantined|published|yanked is status.unknown_state even when signed; a missing member is required", "require releaseDigest to equal the verified manifest's releaseDigest, else status.digest_mismatch", + "require manifestDigest to equal the verified manifest's manifest digest, else status.manifest_mismatch", "verify the signature with the trusted key for keyId: signature.unknown_key, signature.invalid_key or signature.mismatch", - "with a previously accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict; identical bytes are accepted", + "per manifestDigest, against the last accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict; identical bytes are accepted", ], testKey: describeVectorKey(RELEASE_FIXTURE_KEY_ID, VECTOR_KEY_SEED_TEXT, key), + manifests, valid, invalid: [ invalid("state changed after signing", stateChanged, published, mismatch), @@ -143,7 +185,10 @@ async function buildVectors(): Promise { invalid("lower sequence than the accepted status (rollback)", published, yanked, RELEASE_STATUS_CODES.rollback), invalid("same sequence with different content (conflict)", conflicting, yanked, RELEASE_STATUS_CODES.conflict), invalid("unknown state, correctly signed", unknownState, yanked, RELEASE_STATUS_CODES.unknownState), - invalid("status of another release", otherRelease, null, RELEASE_STATUS_CODES.digestMismatch), + invalid("status of another release tree", otherRelease, null, RELEASE_STATUS_CODES.digestMismatch), + invalid("status of another version of the same tree", nextVersionYanked, published, RELEASE_STATUS_CODES.manifestMismatch), + invalid("missing manifestDigest, correctly signed", missingManifestDigest, null, "required"), + invalid("wrong manifestDigest, correctly signed", wrongManifestDigest, null, RELEASE_STATUS_CODES.manifestMismatch), ], }; } @@ -154,6 +199,14 @@ function trustedKeys(): Map { return new Map([[committed.testKey.keyId, fromBase64(committed.testKey.publicKeyBase64)]]); } +function verifyVector(vector: StatusVector) { + return verifyReleaseStatus(vector.status, trustedKeys(), { + releaseDigest: vector.expectedReleaseDigest, + manifestDigest: vector.expectedManifestDigest, + previous: vector.previous, + }); +} + describe("skillx.release-status/v1 cross-language vectors", () => { it.runIf(writeVectorsRequested)("writes the vector file", async () => { await writeVectorFile("release-status-v1.json", await buildVectors()); @@ -163,14 +216,20 @@ describe("skillx.release-status/v1 cross-language vectors", () => { expect(committed).toEqual(await buildVectors()); }); + it.skipIf(writeVectorsRequested)("list verified manifests with their manifest digests", async () => { + expect(committed.manifests.length).toBe(3); + expect(committed.manifests[0]!.manifest.releaseDigest).toBe(committed.manifests[1]!.manifest.releaseDigest); + expect(committed.manifests[0]!.manifestDigest).not.toBe(committed.manifests[1]!.manifestDigest); + for (const entry of committed.manifests) { + expect(await verifyReleaseManifest(entry.manifest, trustedKeys()), entry.name).toEqual({ valid: true, issues: [] }); + expect(await releaseManifestDigest(entry.manifest), entry.name).toBe(entry.manifestDigest); + } + }); + it.skipIf(writeVectorsRequested)("accept every valid vector", async () => { - expect(committed.valid.length).toBe(3); + expect(committed.valid.length).toBe(4); for (const vector of committed.valid) { - const result = await verifyReleaseStatus(vector.status, trustedKeys(), { - releaseDigest: vector.expectedReleaseDigest, - previous: vector.previous, - }); - expect(result, vector.name).toEqual({ valid: true, issues: [] }); + expect(await verifyVector(vector), vector.name).toEqual({ valid: true, issues: [] }); expect(decoder.decode(releaseStatusSigningPayload(vector.status))).toBe(vector.signingPayload); } }); @@ -178,15 +237,11 @@ describe("skillx.release-status/v1 cross-language vectors", () => { it.skipIf(writeVectorsRequested)("reject every invalid vector with its code", async () => { const codes = new Set(); for (const vector of committed.invalid) { - const result = await verifyReleaseStatus(vector.status, trustedKeys(), { - releaseDigest: vector.expectedReleaseDigest, - previous: vector.previous, - }); - expect(result.issues.map((i) => i.code), vector.name).toEqual([vector.expectedCode]); + expect((await verifyVector(vector)).issues.map((i) => i.code), vector.name).toEqual([vector.expectedCode]); codes.add(vector.expectedCode); } expect([...codes].sort()).toEqual( - [...Object.values(RELEASE_STATUS_CODES), RELEASE_SIGNATURE_CODES.mismatch, RELEASE_SIGNATURE_CODES.unknownKey].sort(), + [...Object.values(RELEASE_STATUS_CODES), RELEASE_SIGNATURE_CODES.mismatch, RELEASE_SIGNATURE_CODES.unknownKey, "required"].sort(), ); }); diff --git a/packages/contracts/src/release-status.test.ts b/packages/contracts/src/release-status.test.ts index 3558289..987528b 100644 --- a/packages/contracts/src/release-status.test.ts +++ b/packages/contracts/src/release-status.test.ts @@ -1,12 +1,14 @@ import { describe, expect, it } from "vitest"; import { RELEASE_FIXTURE_KEY_ID, + RELEASE_FIXTURE_MANIFEST_DIGEST, RELEASE_MANIFEST_FIXTURE, RELEASE_MANIFEST_MINIMAL_FIXTURE, + RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST, RELEASE_STATUS_PUBLISHED_FIXTURE, RELEASE_STATUS_YANKED_FIXTURE, } from "./fixtures"; -import { RELEASE_SIGNATURE_CODES, ReleaseManifestError } from "./release-manifest"; +import { RELEASE_SIGNATURE_CODES, ReleaseManifestError, releaseManifestDigest, signReleaseManifest } from "./release-manifest"; import { RELEASE_STATUS_CODES, RELEASE_STATUS_REASON_MAX_LENGTH, @@ -16,10 +18,11 @@ import { validateUnsignedReleaseStatus, verifyReleaseStatus, } from "./release-status"; -import type { SkillxReleaseStatus, UnsignedReleaseStatus } from "./release-status"; +import type { ReleaseStatusVerifyOptions, SkillxReleaseStatus, UnsignedReleaseStatus } from "./release-status"; const decoder = new TextDecoder(); const DIGEST = RELEASE_MANIFEST_FIXTURE.releaseDigest; +const BOUND: ReleaseStatusVerifyOptions = { releaseDigest: DIGEST, manifestDigest: RELEASE_FIXTURE_MANIFEST_DIGEST }; async function generateKey(): Promise<{ privateKey: CryptoKey; publicKey: Uint8Array }> { const pair = (await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"])) as CryptoKeyPair; @@ -40,6 +43,22 @@ async function signedPair() { const codes = (result: { issues: { code: string }[] }) => result.issues.map((i) => i.code); +describe("release manifest digest", () => { + it("is the sha256 of the manifest signing payload and matches the fixture constants", async () => { + expect(await releaseManifestDigest(RELEASE_MANIFEST_FIXTURE)).toBe(RELEASE_FIXTURE_MANIFEST_DIGEST); + expect(await releaseManifestDigest(RELEASE_MANIFEST_MINIMAL_FIXTURE)).toBe(RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST); + }); + + it("ignores the signature but distinguishes two versions of the same tree", async () => { + const { privateKey } = await generateKey(); + const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); + expect(await releaseManifestDigest(signed)).toBe(RELEASE_FIXTURE_MANIFEST_DIGEST); + const nextVersion = { ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }; + expect(nextVersion.releaseDigest).toBe(RELEASE_MANIFEST_FIXTURE.releaseDigest); + expect(await releaseManifestDigest(nextVersion)).not.toBe(RELEASE_FIXTURE_MANIFEST_DIGEST); + }); +}); + describe("skillx.release-status/v1 validation", () => { it("accepts both unsigned fixtures before signing", () => { expect(validateUnsignedReleaseStatus(RELEASE_STATUS_PUBLISHED_FIXTURE)).toEqual({ valid: true, issues: [] }); @@ -51,10 +70,22 @@ describe("skillx.release-status/v1 validation", () => { }); it("reports every required member of an empty object, but not the optional reason", () => { - const paths = validateReleaseStatus({}).issues.filter((i) => i.code === "required").map((i) => i.path); + const result = validateReleaseStatus({}); + const paths = result.issues.filter((i) => i.code === "required").map((i) => i.path); expect(paths.sort()).toEqual( - ["issuedAt", "keyId", "releaseDigest", "schemaVersion", "sequence", "signature", "signatureAlgorithm", "state"].sort(), + [ + "issuedAt", + "keyId", + "manifestDigest", + "releaseDigest", + "schemaVersion", + "sequence", + "signature", + "signatureAlgorithm", + "state", + ].sort(), ); + expect(result.issues.filter((i) => i.path === "manifestDigest").map((i) => i.code)).toEqual(["required"]); }); it("rejects an undefined reason member instead of failing to canonicalize", () => { @@ -85,7 +116,7 @@ describe("skillx.release-status/v1 signing payload", () => { expect(releaseStatusSigningPayload(reordered)).toEqual(releaseStatusSigningPayload(RELEASE_STATUS_PUBLISHED_FIXTURE)); const text = decoder.decode(releaseStatusSigningPayload(RELEASE_STATUS_PUBLISHED_FIXTURE)); expect(text).toBe( - `{"issuedAt":"2026-10-13T04:31:00Z","keyId":"skillx-test-vector-1","releaseDigest":"${DIGEST}","schemaVersion":"skillx.release-status/v1","sequence":1,"signatureAlgorithm":"ed25519","state":"published"}`, + `{"issuedAt":"2026-10-13T04:31:00Z","keyId":"skillx-test-vector-1","manifestDigest":"${RELEASE_FIXTURE_MANIFEST_DIGEST}","releaseDigest":"${DIGEST}","schemaVersion":"skillx.release-status/v1","sequence":1,"signatureAlgorithm":"ed25519","state":"published"}`, ); }); }); @@ -93,9 +124,9 @@ describe("skillx.release-status/v1 signing payload", () => { describe("skillx.release-status/v1 verification", () => { it("verifies a first status and a later one against the accepted status", async () => { const { keys, published, yanked } = await signedPair(); - expect(await verifyReleaseStatus(published, keys, { releaseDigest: DIGEST })).toEqual({ valid: true, issues: [] }); - expect(await verifyReleaseStatus(yanked, keys, { releaseDigest: DIGEST, previous: published })).toEqual({ valid: true, issues: [] }); - expect(await verifyReleaseStatus(yanked, keys, { releaseDigest: DIGEST, previous: yanked })).toEqual({ valid: true, issues: [] }); + expect(await verifyReleaseStatus(published, keys, BOUND)).toEqual({ valid: true, issues: [] }); + expect(await verifyReleaseStatus(yanked, keys, { ...BOUND, previous: published })).toEqual({ valid: true, issues: [] }); + expect(await verifyReleaseStatus(yanked, keys, { ...BOUND, previous: yanked })).toEqual({ valid: true, issues: [] }); }); it.each<[string, (s: SkillxReleaseStatus) => void]>([ @@ -108,56 +139,77 @@ describe("skillx.release-status/v1 verification", () => { const { keys, yanked } = await signedPair(); const tampered = clone(yanked); tamper(tampered); - expect(codes(await verifyReleaseStatus(tampered, keys, { releaseDigest: DIGEST }))).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + expect(codes(await verifyReleaseStatus(tampered, keys, BOUND))).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + }); + + it("rejects a manifestDigest changed after signing", async () => { + const { keys, yanked } = await signedPair(); + const other = await releaseManifestDigest({ ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }); + const tampered = { ...clone(yanked), manifestDigest: other }; + expect(codes(await verifyReleaseStatus(tampered, keys, { ...BOUND, manifestDigest: other }))).toEqual([ + RELEASE_SIGNATURE_CODES.mismatch, + ]); }); it("rejects a status signed by another key under the trusted key ID", async () => { const { yanked } = await signedPair(); const trusted = await generateKey(); - const result = await verifyReleaseStatus(yanked, new Map([[RELEASE_FIXTURE_KEY_ID, trusted.publicKey]]), { releaseDigest: DIGEST }); + const result = await verifyReleaseStatus(yanked, new Map([[RELEASE_FIXTURE_KEY_ID, trusted.publicKey]]), BOUND); expect(codes(result)).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); }); it("rejects an unknown key ID", async () => { const { yanked, keys } = await signedPair(); - const result = await verifyReleaseStatus(yanked, new Map([["skillx-other-key", keys.get(RELEASE_FIXTURE_KEY_ID)!]]), { - releaseDigest: DIGEST, - }); + const result = await verifyReleaseStatus(yanked, new Map([["skillx-other-key", keys.get(RELEASE_FIXTURE_KEY_ID)!]]), BOUND); expect(codes(result)).toEqual([RELEASE_SIGNATURE_CODES.unknownKey]); }); it("rejects a lower sequence than the accepted status", async () => { const { keys, published, yanked } = await signedPair(); - const result = await verifyReleaseStatus(published, keys, { releaseDigest: DIGEST, previous: yanked }); - expect(codes(result)).toEqual([RELEASE_STATUS_CODES.rollback]); + expect(codes(await verifyReleaseStatus(published, keys, { ...BOUND, previous: yanked }))).toEqual([RELEASE_STATUS_CODES.rollback]); }); it("rejects a different status with the accepted sequence", async () => { const { privateKey, keys, yanked } = await signedPair(); const rival = await signReleaseStatus({ ...RELEASE_STATUS_YANKED_FIXTURE, state: "published" }, privateKey); - const result = await verifyReleaseStatus(rival, keys, { releaseDigest: DIGEST, previous: yanked }); - expect(codes(result)).toEqual([RELEASE_STATUS_CODES.conflict]); + expect(codes(await verifyReleaseStatus(rival, keys, { ...BOUND, previous: yanked }))).toEqual([RELEASE_STATUS_CODES.conflict]); }); - it("rejects a status of another release", async () => { + it("rejects a status of another release tree", async () => { const { keys, published } = await signedPair(); - const result = await verifyReleaseStatus(published, keys, { releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest }); + const result = await verifyReleaseStatus(published, keys, { + releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest, + manifestDigest: RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST, + }); expect(codes(result)).toEqual([RELEASE_STATUS_CODES.digestMismatch]); }); + it("rejects a status of another version of the same tree", async () => { + const { privateKey, keys, published } = await signedPair(); + const otherVersion = await releaseManifestDigest({ ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }); + // A status for 1.2.1 does not apply to 1.2.0, although both share the tree digest. + const forOther = await signReleaseStatus({ ...RELEASE_STATUS_YANKED_FIXTURE, manifestDigest: otherVersion }, privateKey); + expect(codes(await verifyReleaseStatus(forOther, keys, BOUND))).toEqual([RELEASE_STATUS_CODES.manifestMismatch]); + // Sequences are tracked per manifest: 1.2.1's status at sequence 1 is not a rollback of 1.2.0's sequence 2. + const firstForOther = await signReleaseStatus({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: otherVersion }, privateKey); + expect(await verifyReleaseStatus(firstForOther, keys, { releaseDigest: DIGEST, manifestDigest: otherVersion })).toEqual({ + valid: true, + issues: [], + }); + expect(published.manifestDigest).not.toBe(otherVersion); + }); + it("fails closed on an unknown state before checking the signature", async () => { const { keys, published } = await signedPair(); const unknown = { ...clone(published), state: "revoked" }; - expect(codes(await verifyReleaseStatus(unknown, keys, { releaseDigest: DIGEST }))).toEqual([RELEASE_STATUS_CODES.unknownState]); + expect(codes(await verifyReleaseStatus(unknown, keys, BOUND))).toEqual([RELEASE_STATUS_CODES.unknownState]); }); - it("throws when the caller passes a previous status of another release", async () => { + it("throws when the caller passes a previous status of another manifest", async () => { const { privateKey, keys, yanked } = await signedPair(); - const foreign = await signReleaseStatus( - { ...RELEASE_STATUS_PUBLISHED_FIXTURE, releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest }, - privateKey, - ); - await expect(verifyReleaseStatus(yanked, keys, { releaseDigest: DIGEST, previous: foreign })).rejects.toBeInstanceOf(ReleaseManifestError); + const otherVersion = await releaseManifestDigest({ ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }); + const foreign = await signReleaseStatus({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: otherVersion }, privateKey); + await expect(verifyReleaseStatus(yanked, keys, { ...BOUND, previous: foreign })).rejects.toBeInstanceOf(ReleaseManifestError); }); it("refuses to sign an unknown state", async () => { diff --git a/packages/contracts/src/release-status.ts b/packages/contracts/src/release-status.ts index c36d42f..e66dcfa 100644 --- a/packages/contracts/src/release-status.ts +++ b/packages/contracts/src/release-status.ts @@ -1,12 +1,16 @@ /** * `skillx.release-status/v1` signed release status contract. * - * The `skillx.release/v1` manifest is signed once per `releaseDigest` and - * never changes. The mutable lifecycle state of that release (published, - * yanked, quarantined, ...) is a separate document, signed with the same - * canonical-bytes and Ed25519 rules (`release-signature.ts`). Each new status - * for a release carries a strictly higher `sequence`, so a consumer that keeps - * the last status it accepted can reject a replayed older one: + * The `skillx.release/v1` manifest is signed once and never changes. The + * mutable lifecycle state of that release (published, yanked, quarantined, + * ...) is a separate document, signed with the same canonical-bytes and + * Ed25519 rules (`release-signature.ts`). A status names exactly one release + * by `manifestDigest` (`releaseManifestDigest`), because the same tree + * (`releaseDigest`) may be released under several identities or versions. + * + * Each new status for a release carries a strictly higher `sequence`, tracked + * per `manifestDigest`, so a consumer that keeps the last status it accepted + * for that manifest can reject a replayed older one: * * - a status whose `sequence` is lower than the last accepted one is a * rollback (`status.rollback`); @@ -43,6 +47,7 @@ export const RELEASE_STATUS_REASON_MAX_LENGTH = 512; export const RELEASE_STATUS_CODES = { unknownState: "status.unknown_state", digestMismatch: "status.digest_mismatch", + manifestMismatch: "status.manifest_mismatch", rollback: "status.rollback", conflict: "status.conflict", } as const; @@ -51,6 +56,8 @@ export interface UnsignedReleaseStatus { schemaVersion: typeof SKILLX_RELEASE_STATUS_SCHEMA_VERSION; /** The `releaseDigest` of the `skillx.release/v1` manifest this status describes. */ releaseDigest: string; + /** `releaseManifestDigest` of that manifest: names exactly one release. */ + manifestDigest: string; state: ReleaseState; /** Optional human-readable explanation, e.g. why a release was yanked. Omit the member when there is none. */ reason?: string; @@ -68,14 +75,17 @@ export interface SkillxReleaseStatus extends UnsignedReleaseStatus { export interface ReleaseStatusVerifyOptions { /** The `releaseDigest` of the verified release manifest the status must describe. */ releaseDigest: string; + /** `releaseManifestDigest` of that verified manifest. */ + manifestDigest: string; /** - * The status this consumer last accepted for the same release, if any. It - * must be a status this function accepted before; an invalid one throws. + * The status this consumer last accepted for the same `manifestDigest`, if + * any. It must be a status this function accepted before; an invalid one, + * or one for another manifest, throws. */ previous?: SkillxReleaseStatus | null; } -const STATUS_FIELDS = ["schemaVersion", "releaseDigest", "state", "issuedAt", "sequence", "keyId", "signatureAlgorithm"] as const; +const STATUS_FIELDS = ["schemaVersion", "releaseDigest", "manifestDigest", "state", "issuedAt", "sequence", "keyId", "signatureAlgorithm"] as const; const OPTIONAL_STATUS_FIELDS = ["reason"] as const; function validateReason(reason: unknown, issues: ValidationIssue[]): void { @@ -97,8 +107,11 @@ function collectIssues(input: unknown, requireSignature: boolean): ValidationIss if (input.schemaVersion !== SKILLX_RELEASE_STATUS_SCHEMA_VERSION) { issues.push(issue("schemaVersion", "unsupported_value", `must equal '${SKILLX_RELEASE_STATUS_SCHEMA_VERSION}'`)); } - if (typeof input.releaseDigest !== "string" || !RELEASE_DIGEST_PATTERN.test(input.releaseDigest)) { - issues.push(issue("releaseDigest", "pattern", "must match sha256:<64 lowercase hex>")); + for (const field of ["releaseDigest", "manifestDigest"] as const) { + const value = input[field]; + if (field in input && (typeof value !== "string" || !RELEASE_DIGEST_PATTERN.test(value))) { + issues.push(issue(field, "pattern", "must match sha256:<64 lowercase hex>")); + } } if ("state" in input && !isReleaseState(input.state)) { issues.push(issue("state", RELEASE_STATUS_CODES.unknownState, `must be one of: ${RELEASE_STATES.join(", ")}`)); @@ -144,7 +157,8 @@ function sameBytes(a: Uint8Array, b: Uint8Array): boolean { } /** - * Validates a signed status, binds it to `options.releaseDigest`, verifies its + * Validates a signed status, binds it to `options.releaseDigest` and + * `options.manifestDigest`, verifies its * signature against `publicKeys` and, when `options.previous` is given, * enforces the monotonic sequence. Fails closed on every malformed input, an * unknown state, an unknown or malformed key, a bad signature, a rollback or @@ -159,7 +173,12 @@ export async function verifyReleaseStatus( if (!structure.valid) return structure; const status = input as SkillxReleaseStatus; if (status.releaseDigest !== options.releaseDigest) { - return validationFail([issue("releaseDigest", RELEASE_STATUS_CODES.digestMismatch, "does not describe the expected release")]); + return validationFail([issue("releaseDigest", RELEASE_STATUS_CODES.digestMismatch, "does not describe the expected release tree")]); + } + if (status.manifestDigest !== options.manifestDigest) { + return validationFail([ + issue("manifestDigest", RELEASE_STATUS_CODES.manifestMismatch, "describes another release manifest of the same tree"), + ]); } const payload = releaseStatusSigningPayload(status); const failure = await verifyPayload(payload, status.signature, status.keyId, publicKeys); @@ -168,8 +187,8 @@ export async function verifyReleaseStatus( const previous = options.previous; if (previous === undefined || previous === null) return validationOk(); const previousResult = validateReleaseStatus(previous); - if (!previousResult.valid || previous.releaseDigest !== options.releaseDigest) { - throw new ReleaseManifestError("previous status is not an accepted status of this release", previousResult.issues); + if (!previousResult.valid || previous.releaseDigest !== options.releaseDigest || previous.manifestDigest !== options.manifestDigest) { + throw new ReleaseManifestError("previous status is not an accepted status of this release manifest", previousResult.issues); } if (status.sequence < previous.sequence) { return validationFail([ From 9527f9c06413a025f33a642503259fde09815bd1 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 12:31:11 +0700 Subject: [PATCH 11/14] feat(skill-tree): expose the pure tree serializer from the rules entry Move the agentkit.skill-tree/v1 serialization (header, per-file lines, UTF-8 path ordering) into a module without Node.js imports, and export serializeTree, compareUtf8 and sortFiles from @skillx/skill-tree/rules. Worker code can now render the exact digest bytes and hash them with WebCrypto. The root entry keeps treeDigest over node:crypto. --- packages/skill-tree/README.md | 7 ++- packages/skill-tree/src/rules.ts | 3 + packages/skill-tree/src/serialize.ts | 76 ++++++-------------------- packages/skill-tree/src/tree-format.ts | 63 +++++++++++++++++++++ 4 files changed, 87 insertions(+), 62 deletions(-) create mode 100644 packages/skill-tree/src/tree-format.ts diff --git a/packages/skill-tree/README.md b/packages/skill-tree/README.md index 3c57358..285dfce 100644 --- a/packages/skill-tree/README.md +++ b/packages/skill-tree/README.md @@ -11,8 +11,11 @@ and its digest. Go parity is pinned by the vendored conformance vectors under - Cloudflare Workers: enable the `nodejs_compat` compatibility flag. The package imports `node:crypto` (`createHash`) for SHA-256. - `@skillx/skill-tree/rules` exports only the pure path, collision and - identity rules (no Node.js import, no archive readers). Worker and browser - code that validates manifests, such as `@skillx/contracts`, imports it. + identity rules plus the canonical tree serialization (`serializeTree`, + `compareUtf8`, `sortFiles`), with no Node.js import, no hashing and no + archive readers. Worker and browser code that validates manifests, such as + `@skillx/contracts`, imports it and hashes `serializeTree(files)` with + WebCrypto to recompute the tree digest. ## Ingest overhead guard (tar) diff --git a/packages/skill-tree/src/rules.ts b/packages/skill-tree/src/rules.ts index 8f60454..646744f 100644 --- a/packages/skill-tree/src/rules.ts +++ b/packages/skill-tree/src/rules.ts @@ -13,3 +13,6 @@ export { checkCollisions, foldKey } from "./collision"; export { UNICODE_VERSION } from "./unicode-15"; export { IDENTITY_PATTERN, RESERVED_PUBLISHER, checkPublisher, validateIdentity } from "./identity"; + +export { MODE_EXECUTABLE, MODE_REGULAR, TREE_SCHEMA, compareUtf8, serializeTree, sortFiles } from "./tree-format"; +export type { FileMode, TreeFile } from "./tree-format"; diff --git a/packages/skill-tree/src/serialize.ts b/packages/skill-tree/src/serialize.ts index 1230d5f..213f008 100644 --- a/packages/skill-tree/src/serialize.ts +++ b/packages/skill-tree/src/serialize.ts @@ -1,66 +1,22 @@ /** - * Canonical serialization and digest of `agentkit.skill-tree/v1` (contract - * 4.8): a header line, then one ` ` line per file, - * sorted by the raw UTF-8 bytes of the path. The tree digest is `sha256:` - * plus the hex SHA-256 of the whole serialization. + * Digest of `agentkit.skill-tree/v1` (contract 4.8): `sha256:` plus the hex + * SHA-256 of the canonical serialization rendered by `tree-format.ts`. */ import { createHash } from "node:crypto"; - -export const TREE_SCHEMA = "agentkit.skill-tree/v1"; - -export const MODE_REGULAR = "100644"; -export const MODE_EXECUTABLE = "100755"; -export type FileMode = typeof MODE_REGULAR | typeof MODE_EXECUTABLE; - -export const MODE_SOURCE_RECORDED = "recorded"; -export const MODE_SOURCE_ASSUMED = "assumed"; -export type ModeSource = typeof MODE_SOURCE_RECORDED | typeof MODE_SOURCE_ASSUMED; - -/** One regular file of a tree. `sha256` is the lowercase hex digest of its content. */ -export interface TreeFile { - path: string; - mode: FileMode; - size: number; - sha256: string; -} - -/** The tree manifest; `files` are sorted by raw UTF-8 path bytes. */ -export interface TreeManifest { - schema: typeof TREE_SCHEMA; - digest: string; - modeSource: ModeSource; - files: TreeFile[]; -} - -/** - * Compares two well-formed strings by their UTF-8 bytes, which is code point - * order (UTF-16 code unit order differs above U+FFFF). - */ -export function compareUtf8(a: string, b: string): number { - const ia = a[Symbol.iterator](); - const ib = b[Symbol.iterator](); - for (;;) { - const ca = ia.next(); - const cb = ib.next(); - if (ca.done || cb.done) return ca.done && cb.done ? 0 : ca.done ? -1 : 1; - const diff = ca.value.codePointAt(0)! - cb.value.codePointAt(0)!; - if (diff !== 0) return diff < 0 ? -1 : 1; - } -} - -export function sortFiles(files: readonly T[]): T[] { - return [...files].sort((a, b) => compareUtf8(a.path, b.path)); -} - -/** - * Renders the canonical serialization. Only a validated file set, whose - * paths are unique, has a defined serialization. - */ -export function serializeTree(files: readonly TreeFile[]): string { - let out = `${TREE_SCHEMA}\n`; - for (const f of sortFiles(files)) out += `${f.mode} ${f.sha256} ${f.size} ${f.path}\n`; - return out; -} +import { serializeTree } from "./tree-format"; +import type { TreeFile } from "./tree-format"; + +export { + MODE_EXECUTABLE, + MODE_REGULAR, + MODE_SOURCE_ASSUMED, + MODE_SOURCE_RECORDED, + TREE_SCHEMA, + compareUtf8, + serializeTree, + sortFiles, +} from "./tree-format"; +export type { FileMode, ModeSource, TreeFile, TreeManifest } from "./tree-format"; export function sha256Hex(data: string | Uint8Array): string { return createHash("sha256").update(data).digest("hex"); diff --git a/packages/skill-tree/src/tree-format.ts b/packages/skill-tree/src/tree-format.ts new file mode 100644 index 0000000..2369e82 --- /dev/null +++ b/packages/skill-tree/src/tree-format.ts @@ -0,0 +1,63 @@ +/** + * The pure canonical serialization of `agentkit.skill-tree/v1` (contract + * 4.8): a header line, then one ` ` line per file, + * sorted by the raw UTF-8 bytes of the path. No hashing and no Node.js + * imports, so Worker and browser code can render the exact bytes the tree + * digest covers and hash them with WebCrypto. + */ + +export const TREE_SCHEMA = "agentkit.skill-tree/v1"; + +export const MODE_REGULAR = "100644"; +export const MODE_EXECUTABLE = "100755"; +export type FileMode = typeof MODE_REGULAR | typeof MODE_EXECUTABLE; + +export const MODE_SOURCE_RECORDED = "recorded"; +export const MODE_SOURCE_ASSUMED = "assumed"; +export type ModeSource = typeof MODE_SOURCE_RECORDED | typeof MODE_SOURCE_ASSUMED; + +/** One regular file of a tree. `sha256` is the lowercase hex digest of its content. */ +export interface TreeFile { + path: string; + mode: FileMode; + size: number; + sha256: string; +} + +/** The tree manifest; `files` are sorted by raw UTF-8 path bytes. */ +export interface TreeManifest { + schema: typeof TREE_SCHEMA; + digest: string; + modeSource: ModeSource; + files: TreeFile[]; +} + +/** + * Compares two well-formed strings by their UTF-8 bytes, which is code point + * order (UTF-16 code unit order differs above U+FFFF). + */ +export function compareUtf8(a: string, b: string): number { + const ia = a[Symbol.iterator](); + const ib = b[Symbol.iterator](); + for (;;) { + const ca = ia.next(); + const cb = ib.next(); + if (ca.done || cb.done) return ca.done && cb.done ? 0 : ca.done ? -1 : 1; + const diff = ca.value.codePointAt(0)! - cb.value.codePointAt(0)!; + if (diff !== 0) return diff < 0 ? -1 : 1; + } +} + +export function sortFiles(files: readonly T[]): T[] { + return [...files].sort((a, b) => compareUtf8(a.path, b.path)); +} + +/** + * Renders the canonical serialization. Only a validated file set, whose + * paths are unique, has a defined serialization. + */ +export function serializeTree(files: readonly TreeFile[]): string { + let out = `${TREE_SCHEMA}\n`; + for (const f of sortFiles(files)) out += `${f.mode} ${f.sha256} ${f.size} ${f.path}\n`; + return out; +} From f33c91c28ceef3a28e75546abe60daa9e9f9f2a8 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 12:31:11 +0700 Subject: [PATCH 12/14] fix(contracts): harden release manifest and status verification Manifest: - sign and verify require releaseDigest to be the tree digest of files (release.tree_digest_mismatch), hashed with WebCrypto - verifiers take the received bytes and require them to equal the canonical JSON (release.non_canonical), rejecting duplicate members, number and escape variants, whitespace, BOM and invalid UTF-8 - release versions reject build metadata and leading zeros (max 128) - signatures must use the single canonical base64 text - validators reject non-plain objects and sparse arrays without throwing Status: - required expiresAt, at most 7 days after issuedAt; verify against a caller-supplied now (status.expired) and the verified manifest - issuedAt not before the manifest createdAt, later than the previous accepted status, and transitions enforced (status.invalid_transition) - reason bounded to 512 UTF-8 bytes without control, format, bidi or separator characters; non-canonical bytes rejected Signed-release fixtures leave the package root export. Vectors become received documents with one expected code each, add expiry, lifetime, transition, non-canonical and UTF-8 path ordering cases, and the docs describe the byte rules, expiry policy and registry-signed trust mapping. --- docs/api-reference.md | 105 ++- packages/contracts/src/canonical-json.test.ts | 40 +- packages/contracts/src/canonical-json.ts | 46 +- packages/contracts/src/fixtures/index.ts | 11 +- .../src/fixtures/release-fixtures.ts | 18 + .../src/fixtures/release-manifest-v1.json | 239 +++++- .../src/fixtures/release-status-fixtures.ts | 59 +- .../src/fixtures/release-status-v1.json | 804 +++++++----------- .../contracts/src/package-manifest.test.ts | 24 +- .../contracts/src/release-manifest.test.ts | 114 ++- packages/contracts/src/release-manifest.ts | 119 ++- packages/contracts/src/release-signature.ts | 59 +- .../src/release-status-vectors.test.ts | 320 ++++--- packages/contracts/src/release-status.test.ts | 333 ++++++-- packages/contracts/src/release-status.ts | 242 ++++-- .../contracts/src/release-vectors.test.ts | 198 ++++- .../test/release-manifest-vectors.test.ts | 16 +- 17 files changed, 1847 insertions(+), 900 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 64e2a2a..d01fabf 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -745,7 +745,7 @@ members; unknown members (including `state`) are rejected at every level. |--------|------|------| | `schemaVersion` | string | `skillx.release/v1` | | `identity` | string | `PACKAGE_NAME_PATTERN`, publisher and slug pass the tree segment rules; `@agentkit/*` is rejected (`identity.reserved_publisher`) | -| `version` | string | semantic version | +| `version` | string | SemVer 2.0 without build metadata (`+...`) or leading zeros in numeric identifiers, at most 128 characters (`RELEASE_VERSION_PATTERN`; stricter than the package-wide `SEMVER_PATTERN`) | | `channel` | string | `stable`, `beta`, `alpha` | | `kind` | string | `skill`, `hook-pack`, `bundle` | | `releaseDigest` | string | `sha256:<64 lowercase hex>`; equals the tree digest of `files` and is the release identity | @@ -754,13 +754,13 @@ members; unknown members (including `state`) are rejected at every level. | `files` | array | non-empty, at most 4096 entries of `{ path, mode, sha256, size }`, sorted by raw UTF-8 path bytes, no duplicates or case-fold collisions; `path` passes the tree path, NFC and noise rules; `mode` is `100644` or `100755`; `sha256` is bare lowercase hex; `size` at most 32 MiB | | `fileCount` | integer | equals `files.length` | | `treeSize` | integer | sum of file sizes, at most 256 MiB | -| `publisher` | object | `{ handle, verifiedBy }`; `handle` equals the identity publisher; `verifiedBy` is `none`, `github` or `admin` (`none` means not publisher-verified) | +| `publisher` | object | `{ handle, verifiedBy }`; `handle` equals the identity publisher; `verifiedBy` is `none`, `github` or `admin` (`none` means not publisher-verified). The signature is a registry key, never a publisher key: AgentKit maps a verified registry signature with `verifiedBy` other than `none` to trust `registry-signed` (not `publisher-signed`), and `none` to `local-unverified` | | `sourceProvenance` | object or `null` | `{ repo, commit, path }`: https URL without credentials, full lowercase hex commit, package root path (`""` for the repository root). `null` when the release was not packed from a recorded git commit; consumers must label such a release "provenance unknown" | | `declaredScripts` | array | paths from `files`, unique and sorted like `files`; informational, never executed by SkillX | | `createdAt` | string | RFC 3339 UTC with whole seconds, e.g. `2026-10-13T04:30:00Z` | | `keyId` | string | `^[a-z0-9][a-z0-9._-]{1,63}$` | | `signatureAlgorithm` | string | `ed25519` | -| `signature` | string | padded standard base64 of the 64-byte Ed25519 signature | +| `signature` | string | padded standard base64 of the 64-byte Ed25519 signature: `^[A-Za-z0-9+/]{85}[AQgw]==$` (the unused trailing bits are zero, so each signature has one accepted text) | ### Signature bytes @@ -779,10 +779,39 @@ space: Go's `encoding/json.Marshal` does not produce these bytes (it HTML-escapes and escapes U+2028/U+2029); a verifier must re-encode with the rules above. +**Served bytes.** A served manifest or status document is exactly the +canonical JSON of the whole document, `signature` included. A verifier parses +the received bytes, re-encodes the parsed value with the rules above and +rejects any byte difference (`release.non_canonical`, `status.non_canonical`). +That one comparison rejects whitespace, member order, duplicate members +(parsers disagree on which wins; Go keeps the last), escape variants, number +forms such as `1e3` or `1.0`, a BOM, trailing bytes and invalid UTF-8, so +every verifier signs and checks the same value. In Go: decode with +`json.Decoder.UseNumber()` into `any`, re-encode, `bytes.Equal`. + +**Domain separation.** There is no prefix before the signed payload. Each +payload carries its own `schemaVersion`, each verifier requires its one schema +version and rejects unknown members, and registry release keys sign only these +two schemas, so a signature over one document type never verifies as the other. + +**Tree digest.** `releaseDigest` must equal `sha256:` plus the hex SHA-256 of +the `agentkit.skill-tree/v1` serialization of `files` +(`agentkit.skill-tree/v1\n`, then ` \n` per file in +UTF-8 path byte order). Signing refuses and verification rejects a mismatch +(`release.tree_digest_mismatch`). `@skillx/skill-tree/rules` exports the pure +`serializeTree`; `releaseTreeDigest` hashes it with WebCrypto. + +A verifier checks, in this order: canonical bytes, structure, tree digest, +signature. Validators and verifiers never throw on document input. + Cross-language vectors: `packages/contracts/src/fixtures/release-manifest-v1.json` -(signed valid manifests with their exact signing payload and file contents, -invalid-signature cases, invalid-manifest cases). The vector key is derived -from a public seed text and must never be trusted outside tests. +(signed valid documents with their exact signing payload, tree serialization +and file contents, including paths whose UTF-8 order differs from UTF-16 +order; rejected documents with their single expected code: tampered, wrong +key, unknown key, Go-escaped, tree digest mismatch, case-variant member and +non-canonical byte variants; structure-only invalid-manifest cases). The +vector key is derived from a public seed text and must never be trusted +outside tests. ### Release status (`skillx.release-status/v1`) @@ -796,9 +825,10 @@ base64). Source: `packages/contracts/src/release-status.ts`. | `releaseDigest` | string | `releaseDigest` of the manifest the status describes | | `manifestDigest` | string | `sha256:<64 lowercase hex>` of that manifest's signing payload (below); names exactly one release | | `state` | string | `draft`, `quarantined`, `published`, `yanked` | -| `reason` | string, optional | 1 to 512 characters, no control characters; omit the member when there is none (never `null`) | -| `issuedAt` | string | RFC 3339 UTC with whole seconds | -| `sequence` | integer | strictly increasing per `manifestDigest`, starting at 1 | +| `reason` | string, optional | 1 to 512 UTF-8 bytes; no `Cc`, `Cf` (including every bidi control), `Zl`, `Zp` or `Cs` code points; omit the member when there is none (never `null` or `""`) | +| `issuedAt` | string | RFC 3339 UTC with whole seconds; not before the manifest's `createdAt` | +| `expiresAt` | string | RFC 3339 UTC with whole seconds; after `issuedAt` and at most 7 days later | +| `sequence` | integer | strictly increasing per `manifestDigest`, starting at 1; the status with sequence 1 is `draft` or `published` | | `keyId` | string | `^[a-z0-9][a-z0-9._-]{1,63}$` | | `signatureAlgorithm` | string | `ed25519` | | `signature` | string | padded standard base64 of the 64-byte Ed25519 signature | @@ -811,27 +841,52 @@ exact bytes the manifest signature covers (`releaseManifestDigest`). It covers (`releaseDigest`) released under another identity or version has a different manifest digest, so a status never applies to a sibling release. -A consumer checks, in this order, and fails closed: - -1. structure; a missing member is `required`; a `state` outside the list above - is `status.unknown_state`, even when correctly signed; -2. `releaseDigest` equals the verified manifest's `releaseDigest`, else `status.digest_mismatch`; -3. `manifestDigest` equals the verified manifest's manifest digest, else `status.manifest_mismatch`; -4. the signature (`signature.unknown_key`, `signature.invalid_key`, `signature.mismatch`); -5. against the last status it accepted for the same `manifestDigest`: a lower +**Freshness.** Every status expires. The registry re-signs the current status +of every release at least daily (a new `sequence`, same state), so a consumer +that fetches normally always holds an unexpired one. A consumer may keep using +an expired status only for content it already installed; an expired status +never authorizes a new resolution or install. + +**Transitions.** Between consecutive statuses of one release: +`draft` to `draft` or `published`; `published` to `published`, `yanked` or +`quarantined`; `yanked` to `yanked` or `published`; `quarantined` to +`quarantined` or `yanked`. Nothing returns to `draft`. When sequences were +skipped, the new state must be reachable in at most the sequence gap steps. + +A consumer verifies the manifest first, then checks the received status +document against it, in this order, and fails closed: + +1. canonical bytes, else `status.non_canonical`; +2. structure: an unknown member is `unknown_field`, a missing member + `required`; a `state` outside the list above is `status.unknown_state`, + even when correctly signed; a bad lifetime is `status.invalid_lifetime`; a + first status that is not `draft` or `published` is + `status.invalid_transition`; +3. `releaseDigest` equals the verified manifest's `releaseDigest`, else `status.digest_mismatch`; +4. `manifestDigest` equals the verified manifest's manifest digest, else `status.manifest_mismatch`; +5. `issuedAt` is not before the manifest's `createdAt`, else `status.issued_before_release`; +6. the signature (`signature.unknown_key`, `signature.invalid_key`, `signature.mismatch`); +7. `now` is not after `expiresAt`, else `status.expired`; +8. against the last status it accepted for the same `manifestDigest`: a lower `sequence` is `status.rollback`; the same `sequence` with different signed - bytes is `status.conflict`; identical bytes are accepted. + bytes is `status.conflict` (identical bytes are accepted); a higher + `sequence` needs a later `issuedAt` (`status.issued_at_regression`) and an + allowed transition (`status.invalid_transition`). Storage (pinned read API work): the registry stores and serves the full signed status document as issued, so `reason` needs no database column; D1 keeps only the state, sequence and issue time used to issue the next status. Cross-language vectors: `packages/contracts/src/fixtures/release-status-v1.json` -(the signed manifests with their manifest digests, including one tree under two -versions; valid statuses with their signing payload and the previously accepted -status; tampered, wrong-key, unknown-key, Go-escaped, rollback, conflict, -unknown-state, other-tree, other-version, missing and wrong `manifestDigest` -cases). Same public test key as the manifest vectors. +(the signed manifest documents with their manifest digests, including one tree +under two versions; each vector names its manifest, the consumer's `now`, the +previously accepted status document and the received document; valid +statuses with their signing payload, including a daily re-sign, a +re-publication, a transition across a skipped sequence and a check exactly at +`expiresAt`; invalid documents with their single expected code, covering every +`status.*` code, tampering, wrong and unknown keys, Go escaping, non-canonical +bytes, a case-variant member, injected empty or `null` reasons and reason +rules). Same public test key as the manifest vectors. ### Database columns (migration `0011_release_tree_digest`) @@ -841,7 +896,9 @@ cases). Same public test key as the manifest vectors. signed status; the state itself stays in `release_state`), and the non-unique index `idx_package_releases_digest`. `packages.visibility` (`public` default, `private`). `publishers.state` (`active` default, -`suspended`, `revoked`). All additive; rollback SQL is in the migration header. +`suspended`, `revoked`). All additive; rollback SQL is in the migration header +and ends by deleting the `0011_release_tree_digest.sql` row from +`d1_migrations`, so the migration can be applied again. --- diff --git a/packages/contracts/src/canonical-json.test.ts b/packages/contracts/src/canonical-json.test.ts index 42ffc23..2898447 100644 --- a/packages/contracts/src/canonical-json.test.ts +++ b/packages/contracts/src/canonical-json.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { CanonicalJsonError, canonicalJson, canonicalJsonBytes } from "./canonical-json"; +import { CanonicalJsonError, canonicalJson, canonicalJsonBytes, parseCanonicalJson } from "./canonical-json"; // Built from code points so no editor or formatter can normalize them away. const E_ACUTE = String.fromCodePoint(0xe9); @@ -56,11 +56,49 @@ describe("canonical JSON", () => { ["a Date", new Date(0)], ["a Map", new Map()], ["a bigint", 1n], + // eslint-disable-next-line no-sparse-arrays + ["a sparse array", [1, , 3]], + ["an array with a trailing hole", new Array(2)], + ["a null-prototype object", Object.create(null)], ])("rejects %s", (_name, value) => { expect(() => canonicalJson(value)).toThrow(CanonicalJsonError); }); it("names the offending path", () => { expect(() => canonicalJson({ files: [{ size: 1.5 }] })).toThrow("files[0].size"); + // eslint-disable-next-line no-sparse-arrays + expect(() => canonicalJson({ files: [1, , 3] })).toThrow("files[1]"); + }); +}); + +describe("parseCanonicalJson", () => { + const canonical = '{"a":[1,"x"],"b":null,"c":true}'; + + it("accepts exactly the canonical text, as a string or UTF-8 bytes", () => { + expect(parseCanonicalJson(canonical)).toEqual({ ok: true, value: { a: [1, "x"], b: null, c: true } }); + expect(parseCanonicalJson(new TextEncoder().encode(canonical)).ok).toBe(true); + const unicode = `{"p":"caf${String.fromCodePoint(0xe9)} ${String.fromCodePoint(0x2028)} & <"}`; + expect(parseCanonicalJson(new TextEncoder().encode(unicode)).ok).toBe(true); + }); + + it.each<[string, string | Uint8Array]>([ + ["whitespace", '{"a": [1,"x"],"b":null,"c":true}'], + ["member order", '{"b":null,"a":[1,"x"],"c":true}'], + ["a duplicate member", '{"a":[1,"x"],"a":[1,"x"],"b":null,"c":true}'], + ["a duplicate member whose last value wins", '{"a":[2],"a":[1,"x"],"b":null,"c":true}'], + ["an exponent", '{"a":[1e0,"x"],"b":null,"c":true}'], + ["a fraction", '{"a":[1.0,"x"],"b":null,"c":true}'], + ["a negative number", '{"a":[-1,"x"],"b":null,"c":true}'], + ["an escaped ASCII character", '{"a":[1,"\\u0078"],"b":null,"c":true}'], + ["an escaped solidus", '{"a":[1,"\\/"],"b":null,"c":true}'], + ["a byte order mark", `${String.fromCodePoint(0xfeff)}${canonical}`], + ["a trailing newline", `${canonical}\n`], + ["invalid UTF-8", new Uint8Array([0x22, 0xff, 0x22])], + ["a UTF-8 byte order mark", new Uint8Array([0xef, 0xbb, 0xbf, ...new TextEncoder().encode(canonical)])], + ["not JSON", "{"], + ["an escaped lone surrogate", '"\\ud800"'], + ])("rejects %s without throwing", (_name, input) => { + const result = parseCanonicalJson(input); + expect(result.ok).toBe(false); }); }); diff --git a/packages/contracts/src/canonical-json.ts b/packages/contracts/src/canonical-json.ts index aa06a99..a96d4cf 100644 --- a/packages/contracts/src/canonical-json.ts +++ b/packages/contracts/src/canonical-json.ts @@ -70,7 +70,13 @@ function encode(value: unknown, path: string): string { return String(value); } if (Array.isArray(value)) { - return `[${value.map((item, i) => encode(item, `${path}[${i}]`)).join(",")}]`; + const items: string[] = []; + // An index loop, unlike `map`, sees the holes of a sparse array. + for (let i = 0; i < value.length; i++) { + if (!(i in value)) throw new CanonicalJsonError(`${path}[${i}]`, "sparse array hole"); + items.push(encode(value[i], `${path}[${i}]`)); + } + return `[${items.join(",")}]`; } if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) { const record = value as Record; @@ -95,3 +101,41 @@ export function canonicalJson(value: unknown): string { export function canonicalJsonBytes(value: unknown): Uint8Array { return new TextEncoder().encode(canonicalJson(value)); } + +export type CanonicalParseResult = { ok: true; value: unknown } | { ok: false; reason: string }; + +/** + * Parses a received signed document and accepts it only when its bytes are + * exactly `canonicalJson` of the parsed value. This one comparison rejects + * whitespace, member-order and escape variants, duplicate members (parsers + * disagree on which wins), number forms such as `1e3`, `1.0` or `-0`, a BOM + * and invalid UTF-8, so every verifier sees the same value. Never throws. + */ +export function parseCanonicalJson(input: string | Uint8Array): CanonicalParseResult { + let text: string; + if (typeof input === "string") { + text = input; + } else { + try { + text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input); + } catch { + return { ok: false, reason: "is not valid UTF-8" }; + } + } + let value: unknown; + try { + value = JSON.parse(text); + } catch { + return { ok: false, reason: "is not JSON" }; + } + let canonical: string; + try { + canonical = canonicalJson(value); + } catch (error) { + return { ok: false, reason: error instanceof Error ? error.message : String(error) }; + } + if (canonical !== text) { + return { ok: false, reason: "is not byte-for-byte the canonical JSON of its value (whitespace, member order, duplicates, escapes or number forms)" }; + } + return { ok: true, value }; +} diff --git a/packages/contracts/src/fixtures/index.ts b/packages/contracts/src/fixtures/index.ts index 7f3ec9d..5d1c8d4 100644 --- a/packages/contracts/src/fixtures/index.ts +++ b/packages/contracts/src/fixtures/index.ts @@ -1,5 +1,10 @@ /** * The single shared validator fixture set for `skillx.package/v1` and siblings. + * + * The signed-release fixtures (`release-fixtures.ts`, + * `release-status-fixtures.ts`) are deliberately not re-exported here: this + * module is part of the package root that Worker code bundles, and those + * fixtures exist only for tests, which import them directly. */ import { @@ -11,15 +16,11 @@ import { ENTITLEMENT_FIXTURES, INGESTION_FIXTURES, } from "./collection-fixtures"; -import { RELEASE_MANIFEST_FIXTURES } from "./release-fixtures"; -import { RELEASE_STATUS_FIXTURES } from "./release-status-fixtures"; import type { FixtureCase } from "./types"; export * from "./types"; export * from "./manifest-fixtures"; export * from "./collection-fixtures"; -export * from "./release-fixtures"; -export * from "./release-status-fixtures"; export const ALL_FIXTURES: FixtureCase[] = [ ...PACKAGE_MANIFEST_FIXTURES, @@ -27,6 +28,4 @@ export const ALL_FIXTURES: FixtureCase[] = [ ...COLLECTION_MEMBER_FIXTURES, ...ENTITLEMENT_FIXTURES, ...INGESTION_FIXTURES, - ...RELEASE_MANIFEST_FIXTURES, - ...RELEASE_STATUS_FIXTURES, ]; diff --git a/packages/contracts/src/fixtures/release-fixtures.ts b/packages/contracts/src/fixtures/release-fixtures.ts index ae4828e..1b75d2d 100644 --- a/packages/contracts/src/fixtures/release-fixtures.ts +++ b/packages/contracts/src/fixtures/release-fixtures.ts @@ -122,6 +122,24 @@ export const RELEASE_MANIFEST_FIXTURES: FixtureCase[] = [ value: signedReleaseDraft(RELEASE_MANIFEST_MINIMAL_FIXTURE), expectedValid: true, }, + rejected("rejects a version with build metadata", "pattern", (d) => { + d.version = "1.2.0+build.7"; + }), + rejected("rejects a version with a leading zero", "pattern", (d) => { + d.version = "01.2.0"; + }), + rejected("rejects a numeric prerelease identifier with a leading zero", "pattern", (d) => { + d.version = "1.2.0-beta.01"; + }), + rejected("rejects an empty prerelease identifier", "pattern", (d) => { + d.version = "1.2.0-beta..1"; + }), + rejected("rejects a version longer than 128 characters", "pattern", (d) => { + d.version = `1.2.0-${"a".repeat(123)}`; + }), + rejected("rejects a non-integer number anywhere", "type", (d) => { + d.artifactSize = 1.5; + }), rejected("rejects an unknown top-level field", "unknown_field", (d) => { d.price = 0; }), diff --git a/packages/contracts/src/fixtures/release-manifest-v1.json b/packages/contracts/src/fixtures/release-manifest-v1.json index e3963ad..e58ebe1 100644 --- a/packages/contracts/src/fixtures/release-manifest-v1.json +++ b/packages/contracts/src/fixtures/release-manifest-v1.json @@ -1,6 +1,12 @@ { "description": "skillx.release/v1 signed manifest vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", "canonicalization": "signature = padded base64 Ed25519 over UTF-8 canonical JSON (RFC 8785 subset: members sorted by UTF-16 code units, no whitespace, non-negative safe integers only, only quote/backslash/C0 controls escaped, everything else raw UTF-8) of the manifest without its signature member; see packages/contracts/src/canonical-json.ts", + "verifierRules": [ + "parse document and re-encode it canonically; any byte difference (whitespace, member order, duplicate member, escapes, number form, BOM, trailing bytes, invalid UTF-8) is release.non_canonical", + "validate the structure; an unknown member, including one differing from a known member only by case, is unknown_field", + "require releaseDigest == 'sha256:' + hex SHA-256 of treeSerialization (agentkit.skill-tree/v1 of files), else release.tree_digest_mismatch", + "verify the signature with the trusted key for keyId over the canonical JSON without signature: signature.unknown_key, signature.invalid_key or signature.mismatch" + ], "testKey": { "keyId": "skillx-test-vector-1", "seedText": "skillx.release/v1 test vector key; public, never trusted", @@ -11,6 +17,7 @@ "valid": [ { "name": "publisher-verified multi-file release", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@publisher/review-guardrails", @@ -61,6 +68,7 @@ }, "signingPayload": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", "signingPayloadSha256": "64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", + "treeSerialization": "agentkit.skill-tree/v1\n100644 bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11 102 SKILL.md\n100644 d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f 34 references/café & notes
.md\n100755 b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c 18 scripts/check.sh\n", "fileContentsBase64": { "SKILL.md": "LS0tCm5hbWU6IHJldmlldy1ndWFyZHJhaWxzCmRlc2NyaXB0aW9uOiBSZXZpZXcgZ3VhcmRyYWlscyBmb3IgcHVsbCByZXF1ZXN0cy4KLS0tCiMgUmV2aWV3IGd1YXJkcmFpbHMK", "references/café & notes
.md": "Tm90ZXMgd2l0aCBhIGxpbmUgc2VwYXJhdG9yIHBhdGguCg==", @@ -69,6 +77,7 @@ }, { "name": "unverified minimal prerelease with unknown provenance", + "document": "{\"artifactDigest\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"artifactSize\":1536,\"channel\":\"beta\",\"createdAt\":\"2026-10-13T05:00:00Z\",\"declaredScripts\":[],\"fileCount\":1,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d\",\"size\":44}],\"identity\":\"@solo.dev/tiny_skill\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"solo.dev\",\"verifiedBy\":\"none\"},\"releaseDigest\":\"sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"tEUDJBptJIcz6nVFciKaixGz3bUvTe5ZAVI6cYBFtWsK75DU3bTqSfauO8Tf2Te85zfHA3IbY0FOYPviKWS/Dw==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":null,\"treeSize\":44,\"version\":\"0.1.0-beta.1\"}", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@solo.dev/tiny_skill", @@ -101,18 +110,150 @@ }, "signingPayload": "{\"artifactDigest\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"artifactSize\":1536,\"channel\":\"beta\",\"createdAt\":\"2026-10-13T05:00:00Z\",\"declaredScripts\":[],\"fileCount\":1,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d\",\"size\":44}],\"identity\":\"@solo.dev/tiny_skill\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"solo.dev\",\"verifiedBy\":\"none\"},\"releaseDigest\":\"sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544\",\"schemaVersion\":\"skillx.release/v1\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":null,\"treeSize\":44,\"version\":\"0.1.0-beta.1\"}", "signingPayloadSha256": "5979df5f8e7c400ac422183440040742bb34274bca37c9347d100948871e3596", + "treeSerialization": "agentkit.skill-tree/v1\n100644 7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d 44 SKILL.md\n", "fileContentsBase64": { "SKILL.md": "LS0tCm5hbWU6IHRpbnkKZGVzY3JpcHRpb246IFRpbnkgc2tpbGwuCi0tLQo=" } + }, + { + "name": "paths ordered by UTF-8 bytes, not UTF-16 code units", + "document": "{\"artifactDigest\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"artifactSize\":1536,\"channel\":\"beta\",\"createdAt\":\"2026-10-13T05:00:00Z\",\"declaredScripts\":[],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"d96db5b1cde1b0fa4e0d0d7996aa31085ded7851a7a520771da1b49555552159\",\"size\":60},{\"mode\":\"100644\",\"path\":\"references/。.md\",\"sha256\":\"8fbbd1833dfc6c602ce9d3ea7d07b26a73688c7fca36cb0db0074df88755563a\",\"size\":33},{\"mode\":\"100644\",\"path\":\"references/😀.md\",\"sha256\":\"1a5312dbfaeaec33318a1283f61e0bf78fa9657f9f238b20773ce330986e31ec\",\"size\":20}],\"identity\":\"@solo.dev/path-order\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"solo.dev\",\"verifiedBy\":\"none\"},\"releaseDigest\":\"sha256:b5b69211197fee79a09fa386eb57a32709d5bb07a7e099c409e81245d97a8453\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"KYrORybgBetWrvlnOQ0Rme0j4gaRRBNqVrC+KoukqrJnckNlYzAZyc0y1IrUumS9LOJBmvCRehfk7n7irufgDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":null,\"treeSize\":113,\"version\":\"0.2.0\"}", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@solo.dev/path-order", + "version": "0.2.0", + "channel": "beta", + "kind": "skill", + "releaseDigest": "sha256:b5b69211197fee79a09fa386eb57a32709d5bb07a7e099c409e81245d97a8453", + "artifactDigest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "artifactSize": 1536, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "d96db5b1cde1b0fa4e0d0d7996aa31085ded7851a7a520771da1b49555552159", + "size": 60 + }, + { + "path": "references/。.md", + "mode": "100644", + "sha256": "8fbbd1833dfc6c602ce9d3ea7d07b26a73688c7fca36cb0db0074df88755563a", + "size": 33 + }, + { + "path": "references/😀.md", + "mode": "100644", + "sha256": "1a5312dbfaeaec33318a1283f61e0bf78fa9657f9f238b20773ce330986e31ec", + "size": 20 + } + ], + "fileCount": 3, + "treeSize": 113, + "publisher": { + "handle": "solo.dev", + "verifiedBy": "none" + }, + "sourceProvenance": null, + "declaredScripts": [], + "createdAt": "2026-10-13T05:00:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "KYrORybgBetWrvlnOQ0Rme0j4gaRRBNqVrC+KoukqrJnckNlYzAZyc0y1IrUumS9LOJBmvCRehfk7n7irufgDg==" + }, + "signingPayload": "{\"artifactDigest\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"artifactSize\":1536,\"channel\":\"beta\",\"createdAt\":\"2026-10-13T05:00:00Z\",\"declaredScripts\":[],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"d96db5b1cde1b0fa4e0d0d7996aa31085ded7851a7a520771da1b49555552159\",\"size\":60},{\"mode\":\"100644\",\"path\":\"references/。.md\",\"sha256\":\"8fbbd1833dfc6c602ce9d3ea7d07b26a73688c7fca36cb0db0074df88755563a\",\"size\":33},{\"mode\":\"100644\",\"path\":\"references/😀.md\",\"sha256\":\"1a5312dbfaeaec33318a1283f61e0bf78fa9657f9f238b20773ce330986e31ec\",\"size\":20}],\"identity\":\"@solo.dev/path-order\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"solo.dev\",\"verifiedBy\":\"none\"},\"releaseDigest\":\"sha256:b5b69211197fee79a09fa386eb57a32709d5bb07a7e099c409e81245d97a8453\",\"schemaVersion\":\"skillx.release/v1\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":null,\"treeSize\":113,\"version\":\"0.2.0\"}", + "signingPayloadSha256": "4ae4c01ccc7f9f2d15291a1392040594397040f5fdb145b8bba6b1df1f9b2957", + "treeSerialization": "agentkit.skill-tree/v1\n100644 d96db5b1cde1b0fa4e0d0d7996aa31085ded7851a7a520771da1b49555552159 60 SKILL.md\n100644 8fbbd1833dfc6c602ce9d3ea7d07b26a73688c7fca36cb0db0074df88755563a 33 references/。.md\n100644 1a5312dbfaeaec33318a1283f61e0bf78fa9657f9f238b20773ce330986e31ec 20 references/😀.md\n", + "fileContentsBase64": { + "SKILL.md": "LS0tCm5hbWU6IHBhdGgtb3JkZXIKZGVzY3JpcHRpb246IFBhdGggb3JkZXJpbmcgdmVjdG9yLgotLS0K", + "references/。.md": "SGFsZndpZHRoIGlkZW9ncmFwaGljIGZ1bGwgc3RvcC4K", + "references/😀.md": "QXN0cmFsLXBsYW5lIGVtb2ppLgo=" + } } ], - "invalidSignature": [ + "rejected": [ { "name": "version changed after signing", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.1\"}", + "expectedCode": "signature.mismatch" + }, + { + "name": "flipped signature byte", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eXdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "signature.mismatch" + }, + { + "name": "signed by another key under the trusted key ID", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"xmFvEJWdvdJjY15sAF4O2YqthxSCz5ZDz6XA0NivxR7YVKbsUEx7bIt6MfrBC+B/RBR+8TxEXwg9zI8wku4kDQ==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "signature.mismatch" + }, + { + "name": "unknown key ID", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-unknown-key\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "signature.unknown_key" + }, + { + "name": "signature over an encoding/json-escaped payload", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"9dEh1PX7CiqOi6gaClemhI+a0hF5HGSok+q6zFCsVwn2a3stv8J4WARheAWzRTIHJTwKBGT39lAgKNygoPgRAQ==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "signature.mismatch" + }, + { + "name": "releaseDigest is not the tree digest of files, correctly signed", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"3f37segS8H/FZ+TY7GG+T0dObWAbq/6zEHl3yJfGVzNviuHyNvikJym/MIn6jkEu4Keg9iCeDzj30lzLRaXbCA==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "release.tree_digest_mismatch" + }, + { + "name": "file hash changed, correctly signed", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b10\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"/pAl8KEgJ6S5epAQaq4OClDYEPlxnoIITMF5zCeQcwBUGW8bnS6X1au32CDH5cRBpS9WR7vP+ieTtfVj+Q5WDQ==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "release.tree_digest_mismatch" + }, + { + "name": "member differing from a known one only by case", + "document": "{\"Version\":\"1.2.0\",\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "unknown_field" + }, + { + "name": "pretty-printed", + "document": "{\n \"schemaVersion\": \"skillx.release/v1\",\n \"identity\": \"@publisher/review-guardrails\",\n \"version\": \"1.2.0\",\n \"channel\": \"stable\",\n \"kind\": \"skill\",\n \"releaseDigest\": \"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\n \"artifactDigest\": \"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\n \"artifactSize\": 10240,\n \"files\": [\n {\n \"path\": \"SKILL.md\",\n \"mode\": \"100644\",\n \"sha256\": \"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\n \"size\": 102\n },\n {\n \"path\": \"references/café & notes
.md\",\n \"mode\": \"100644\",\n \"sha256\": \"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\n \"size\": 34\n },\n {\n \"path\": \"scripts/check.sh\",\n \"mode\": \"100755\",\n \"sha256\": \"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\n \"size\": 18\n }\n ],\n \"fileCount\": 3,\n \"treeSize\": 154,\n \"publisher\": {\n \"handle\": \"publisher\",\n \"verifiedBy\": \"github\"\n },\n \"sourceProvenance\": {\n \"repo\": \"https://github.com/publisher/review-guardrails\",\n \"commit\": \"0123456789abcdef0123456789abcdef01234567\",\n \"path\": \"skills/review-guardrails\"\n },\n \"declaredScripts\": [\n \"scripts/check.sh\"\n ],\n \"createdAt\": \"2026-10-13T04:30:00Z\",\n \"keyId\": \"skillx-test-vector-1\",\n \"signatureAlgorithm\": \"ed25519\",\n \"signature\": \"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\"\n}", + "expectedCode": "release.non_canonical" + }, + { + "name": "members in insertion order", + "document": "{\"schemaVersion\":\"skillx.release/v1\",\"identity\":\"@publisher/review-guardrails\",\"version\":\"1.2.0\",\"channel\":\"stable\",\"kind\":\"skill\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"files\":[{\"path\":\"SKILL.md\",\"mode\":\"100644\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"path\":\"references/café & notes
.md\",\"mode\":\"100644\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"path\":\"scripts/check.sh\",\"mode\":\"100755\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"fileCount\":3,\"treeSize\":154,\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"sourceProvenance\":{\"repo\":\"https://github.com/publisher/review-guardrails\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\"},\"declaredScripts\":[\"scripts/check.sh\"],\"createdAt\":\"2026-10-13T04:30:00Z\",\"keyId\":\"skillx-test-vector-1\",\"signatureAlgorithm\":\"ed25519\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\"}", + "expectedCode": "release.non_canonical" + }, + { + "name": "duplicate member", + "document": "{\"version\":\"9.9.9\",\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "release.non_canonical" + }, + { + "name": "exponent number form", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":1.024e4,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "release.non_canonical" + }, + { + "name": "escaped solidus", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts\\/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "release.non_canonical" + }, + { + "name": "byte order mark", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "release.non_canonical" + }, + { + "name": "trailing newline", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}\n", + "expectedCode": "release.non_canonical" + } + ], + "invalidManifest": [ + { + "name": "rejects a version with build metadata", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@publisher/review-guardrails", - "version": "1.2.1", + "version": "1.2.0+build.7", "channel": "stable", "kind": "skill", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", @@ -155,16 +296,16 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==" + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, - "expectedCode": "signature.mismatch" + "expectedCode": "pattern" }, { - "name": "flipped signature byte", + "name": "rejects a version with a leading zero", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@publisher/review-guardrails", - "version": "1.2.0", + "version": "01.2.0", "channel": "stable", "kind": "skill", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", @@ -207,16 +348,16 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "eXdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==" + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, - "expectedCode": "signature.mismatch" + "expectedCode": "pattern" }, { - "name": "signed by another key under the trusted key ID", + "name": "rejects a numeric prerelease identifier with a leading zero", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@publisher/review-guardrails", - "version": "1.2.0", + "version": "1.2.0-beta.01", "channel": "stable", "kind": "skill", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", @@ -259,16 +400,16 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "xmFvEJWdvdJjY15sAF4O2YqthxSCz5ZDz6XA0NivxR7YVKbsUEx7bIt6MfrBC+B/RBR+8TxEXwg9zI8wku4kDQ==" + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, - "expectedCode": "signature.mismatch" + "expectedCode": "pattern" }, { - "name": "unknown key ID", + "name": "rejects an empty prerelease identifier", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@publisher/review-guardrails", - "version": "1.2.0", + "version": "1.2.0-beta..1", "channel": "stable", "kind": "skill", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", @@ -309,18 +450,18 @@ "scripts/check.sh" ], "createdAt": "2026-10-13T04:30:00Z", - "keyId": "skillx-unknown-key", + "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==" + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, - "expectedCode": "signature.unknown_key" + "expectedCode": "pattern" }, { - "name": "signature over an encoding/json-escaped payload", + "name": "rejects a version longer than 128 characters", "manifest": { "schemaVersion": "skillx.release/v1", "identity": "@publisher/review-guardrails", - "version": "1.2.0", + "version": "1.2.0-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "channel": "stable", "kind": "skill", "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", @@ -363,12 +504,62 @@ "createdAt": "2026-10-13T04:30:00Z", "keyId": "skillx-test-vector-1", "signatureAlgorithm": "ed25519", - "signature": "9dEh1PX7CiqOi6gaClemhI+a0hF5HGSok+q6zFCsVwn2a3stv8J4WARheAWzRTIHJTwKBGT39lAgKNygoPgRAQ==" + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, - "expectedCode": "signature.mismatch" - } - ], - "invalidManifest": [ + "expectedCode": "pattern" + }, + { + "name": "rejects a non-integer number anywhere", + "manifest": { + "schemaVersion": "skillx.release/v1", + "identity": "@publisher/review-guardrails", + "version": "1.2.0", + "channel": "stable", + "kind": "skill", + "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", + "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "artifactSize": 1.5, + "files": [ + { + "path": "SKILL.md", + "mode": "100644", + "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", + "size": 102 + }, + { + "path": "references/café & notes
.md", + "mode": "100644", + "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", + "size": 34 + }, + { + "path": "scripts/check.sh", + "mode": "100755", + "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", + "size": 18 + } + ], + "fileCount": 3, + "treeSize": 154, + "publisher": { + "handle": "publisher", + "verifiedBy": "github" + }, + "sourceProvenance": { + "repo": "https://github.com/publisher/review-guardrails", + "commit": "0123456789abcdef0123456789abcdef01234567", + "path": "skills/review-guardrails" + }, + "declaredScripts": [ + "scripts/check.sh" + ], + "createdAt": "2026-10-13T04:30:00Z", + "keyId": "skillx-test-vector-1", + "signatureAlgorithm": "ed25519", + "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "expectedCode": "type" + }, { "name": "rejects an unknown top-level field", "manifest": { diff --git a/packages/contracts/src/fixtures/release-status-fixtures.ts b/packages/contracts/src/fixtures/release-status-fixtures.ts index 2926453..b3229fa 100644 --- a/packages/contracts/src/fixtures/release-status-fixtures.ts +++ b/packages/contracts/src/fixtures/release-status-fixtures.ts @@ -26,6 +26,7 @@ export const RELEASE_STATUS_PUBLISHED_FIXTURE: UnsignedReleaseStatus = { manifestDigest: RELEASE_FIXTURE_MANIFEST_DIGEST, state: "published", issuedAt: "2026-10-13T04:31:00Z", + expiresAt: "2026-10-14T04:31:00Z", sequence: 1, keyId: RELEASE_FIXTURE_KEY_ID, signatureAlgorithm: "ed25519", @@ -39,6 +40,7 @@ export const RELEASE_STATUS_YANKED_FIXTURE: UnsignedReleaseStatus = { state: "yanked", reason: RELEASE_STATUS_FIXTURE_REASON, issuedAt: "2026-10-14T09:00:00Z", + expiresAt: "2026-10-15T09:00:00Z", sequence: 2, keyId: RELEASE_FIXTURE_KEY_ID, signatureAlgorithm: "ed25519", @@ -50,16 +52,36 @@ function signedStatusDraft(base: UnsignedReleaseStatus): MutableFixture { return copy as unknown as MutableFixture; } -function rejected(name: string, expectedIssueCode: string, mutate: (draft: MutableFixture) => void): FixtureCase { +function variant(mutate: (draft: MutableFixture) => void): MutableFixture { const draft = signedStatusDraft(RELEASE_STATUS_YANKED_FIXTURE); mutate(draft); - return { name, group: "release-status", value: draft, expectedValid: false, expectedIssueCode }; + return draft; +} + +function accepted(name: string, mutate: (draft: MutableFixture) => void): FixtureCase { + return { name, group: "release-status", value: variant(mutate), expectedValid: true }; +} + +function rejected(name: string, expectedIssueCode: string, mutate: (draft: MutableFixture) => void): FixtureCase { + return { name, group: "release-status", value: variant(mutate), expectedValid: false, expectedIssueCode }; } +/** A reason of exactly 512 UTF-8 bytes but only 256 UTF-16 code units. */ +export const RELEASE_STATUS_MAX_REASON = String.fromCodePoint(0xe9).repeat(256); + /** Structure-only cases for `validateReleaseStatus`; signatures are placeholders. */ export const RELEASE_STATUS_FIXTURES: FixtureCase[] = [ { name: "valid status without a reason", group: "release-status", value: signedStatusDraft(RELEASE_STATUS_PUBLISHED_FIXTURE), expectedValid: true }, { name: "valid status with a reason", group: "release-status", value: signedStatusDraft(RELEASE_STATUS_YANKED_FIXTURE), expectedValid: true }, + accepted("accepts a lifetime of exactly 7 days", (d) => { + d.expiresAt = "2026-10-21T09:00:00Z"; + }), + accepted("accepts a reason of exactly 512 UTF-8 bytes", (d) => { + d.reason = RELEASE_STATUS_MAX_REASON; + }), + accepted("accepts a later status in a state no first status may have", (d) => { + d.state = "quarantined"; + }), rejected("rejects an unknown state", "status.unknown_state", (d) => { d.state = "revoked"; }), @@ -67,7 +89,38 @@ export const RELEASE_STATUS_FIXTURES: FixtureCase[] = [ delete d.state; }), rejected("rejects an unknown member", "unknown_field", (d) => { - d.expiresAt = "2027-01-01T00:00:00Z"; + d.ttl = 86400; + }), + rejected("rejects a missing expiresAt", "required", (d) => { + delete d.expiresAt; + }), + rejected("rejects an expiresAt before issuedAt", "status.invalid_lifetime", (d) => { + d.expiresAt = "2026-10-14T08:59:59Z"; + }), + rejected("rejects an expiresAt equal to issuedAt", "status.invalid_lifetime", (d) => { + d.expiresAt = d.issuedAt; + }), + rejected("rejects a lifetime longer than 7 days", "status.invalid_lifetime", (d) => { + d.expiresAt = "2026-10-21T09:00:01Z"; + }), + rejected("rejects a first status that is yanked", "status.invalid_transition", (d) => { + d.sequence = 1; + }), + rejected("rejects a reason over 512 UTF-8 bytes", "length", (d) => { + d.reason = `${RELEASE_STATUS_MAX_REASON}a`; + }), + rejected("rejects a bidi override in the reason", "format", (d) => { + d.reason = `safe${String.fromCodePoint(0x202e)}hs.kcehc`; + }), + rejected("rejects a line separator in the reason", "format", (d) => { + d.reason = `line one${String.fromCodePoint(0x2028)}line two`; + }), + rejected("rejects a zero-width format character in the reason", "format", (d) => { + d.reason = `yan${String.fromCodePoint(0x200b)}ked`; + }), + rejected("rejects a sparse array value", "type", (d) => { + // eslint-disable-next-line no-sparse-arrays + d.reason = [, "x"] as unknown as string; }), rejected("rejects a missing signature", "required", (d) => { delete d.signature; diff --git a/packages/contracts/src/fixtures/release-status-v1.json b/packages/contracts/src/fixtures/release-status-v1.json index 202cb53..4a911db 100644 --- a/packages/contracts/src/fixtures/release-status-v1.json +++ b/packages/contracts/src/fixtures/release-status-v1.json @@ -1,13 +1,17 @@ { "description": "skillx.release-status/v1 signed status vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", - "canonicalization": "same as skillx.release/v1: padded base64 Ed25519 over the UTF-8 canonical JSON of the status without its signature member; an absent reason is omitted, never null", + "canonicalization": "same as skillx.release/v1: padded base64 Ed25519 over the UTF-8 canonical JSON of the status without its signature member; an absent reason is omitted, never null or empty; a served document is exactly the canonical JSON including signature", "manifestDigestRule": "manifestDigest = 'sha256:' + lowercase hex SHA-256 of the manifest signing payload: the UTF-8 canonical JSON of the skillx.release/v1 manifest without its signature member (the exact bytes its signature covers)", "consumerRules": [ - "validate the structure; a state outside draft|quarantined|published|yanked is status.unknown_state even when signed; a missing member is required", + "verify the manifest document first (skillx.release/v1 vectors); statuses are checked against that verified manifest", + "re-encode the received document canonically; any byte difference is status.non_canonical", + "validate the structure: unknown member unknown_field; missing member required; state outside draft|quarantined|published|yanked status.unknown_state even when signed; reason 1..512 UTF-8 bytes (length, null is type) without Cc, Cf (incl. bidi controls), Zl, Zp or Cs code points (format); issuedAt < expiresAt <= issuedAt + 7 days else status.invalid_lifetime; sequence 1 must be draft or published else status.invalid_transition", "require releaseDigest to equal the verified manifest's releaseDigest, else status.digest_mismatch", "require manifestDigest to equal the verified manifest's manifest digest, else status.manifest_mismatch", + "require issuedAt >= the manifest createdAt, else status.issued_before_release", "verify the signature with the trusted key for keyId: signature.unknown_key, signature.invalid_key or signature.mismatch", - "per manifestDigest, against the last accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict; identical bytes are accepted" + "require now <= expiresAt, else status.expired; an expired status may only keep already-installed content, never authorize a new resolution or install", + "per manifestDigest, against the last accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict (identical bytes are accepted); a higher sequence needs a later issuedAt (status.issued_at_regression) and a state reachable in at most (sequence gap) steps of draft->draft|published, published->published|yanked|quarantined, yanked->yanked|published, quarantined->quarantined|yanked (status.invalid_transition)" ], "testKey": { "keyId": "skillx-test-vector-1", @@ -19,588 +23,366 @@ "manifests": [ { "name": "1.2.0 of the multi-file tree", - "manifest": { - "schemaVersion": "skillx.release/v1", - "identity": "@publisher/review-guardrails", - "version": "1.2.0", - "channel": "stable", - "kind": "skill", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "artifactSize": 10240, - "files": [ - { - "path": "SKILL.md", - "mode": "100644", - "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", - "size": 102 - }, - { - "path": "references/café & notes
.md", - "mode": "100644", - "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", - "size": 34 - }, - { - "path": "scripts/check.sh", - "mode": "100755", - "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", - "size": 18 - } - ], - "fileCount": 3, - "treeSize": 154, - "publisher": { - "handle": "publisher", - "verifiedBy": "github" - }, - "sourceProvenance": { - "repo": "https://github.com/publisher/review-guardrails", - "commit": "0123456789abcdef0123456789abcdef01234567", - "path": "skills/review-guardrails" - }, - "declaredScripts": [ - "scripts/check.sh" - ], - "createdAt": "2026-10-13T04:30:00Z", - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==" - }, + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDg==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc" }, { "name": "1.2.1 of the same tree", - "manifest": { - "schemaVersion": "skillx.release/v1", - "identity": "@publisher/review-guardrails", - "version": "1.2.1", - "channel": "stable", - "kind": "skill", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "artifactDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "artifactSize": 10240, - "files": [ - { - "path": "SKILL.md", - "mode": "100644", - "sha256": "bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11", - "size": 102 - }, - { - "path": "references/café & notes
.md", - "mode": "100644", - "sha256": "d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f", - "size": 34 - }, - { - "path": "scripts/check.sh", - "mode": "100755", - "sha256": "b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c", - "size": 18 - } - ], - "fileCount": 3, - "treeSize": 154, - "publisher": { - "handle": "publisher", - "verifiedBy": "github" - }, - "sourceProvenance": { - "repo": "https://github.com/publisher/review-guardrails", - "commit": "0123456789abcdef0123456789abcdef01234567", - "path": "skills/review-guardrails" - }, - "declaredScripts": [ - "scripts/check.sh" - ], - "createdAt": "2026-10-13T04:30:00Z", - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "luJjP0RMcubnl6QZf3boxrHDen2iAchsvxQ19zqAFkE79xqSO2DpevYIEDGmeL5Qw0LDOrbGxuc/uqHvWBqXCw==" - }, + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"luJjP0RMcubnl6QZf3boxrHDen2iAchsvxQ19zqAFkE79xqSO2DpevYIEDGmeL5Qw0LDOrbGxuc/uqHvWBqXCw==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.1\"}", "manifestDigest": "sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9" }, { "name": "minimal release", - "manifest": { - "schemaVersion": "skillx.release/v1", - "identity": "@solo.dev/tiny_skill", - "version": "0.1.0-beta.1", - "channel": "beta", - "kind": "skill", - "releaseDigest": "sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544", - "artifactDigest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "artifactSize": 1536, - "files": [ - { - "path": "SKILL.md", - "mode": "100644", - "sha256": "7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d", - "size": 44 - } - ], - "fileCount": 1, - "treeSize": 44, - "publisher": { - "handle": "solo.dev", - "verifiedBy": "none" - }, - "sourceProvenance": null, - "declaredScripts": [], - "createdAt": "2026-10-13T05:00:00Z", - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "tEUDJBptJIcz6nVFciKaixGz3bUvTe5ZAVI6cYBFtWsK75DU3bTqSfauO8Tf2Te85zfHA3IbY0FOYPviKWS/Dw==" - }, + "document": "{\"artifactDigest\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"artifactSize\":1536,\"channel\":\"beta\",\"createdAt\":\"2026-10-13T05:00:00Z\",\"declaredScripts\":[],\"fileCount\":1,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"7822f865b6a9be616262a844ffb54085d095857dcf35a172150523db31a3726d\",\"size\":44}],\"identity\":\"@solo.dev/tiny_skill\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"solo.dev\",\"verifiedBy\":\"none\"},\"releaseDigest\":\"sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"tEUDJBptJIcz6nVFciKaixGz3bUvTe5ZAVI6cYBFtWsK75DU3bTqSfauO8Tf2Te85zfHA3IbY0FOYPviKWS/Dw==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":null,\"treeSize\":44,\"version\":\"0.1.0-beta.1\"}", "manifestDigest": "sha256:5979df5f8e7c400ac422183440040742bb34274bca37c9347d100948871e3596" } ], "valid": [ { "name": "first status of a release", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", "previous": null, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" - }, - "signingPayload": "{\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", - "signingPayloadSha256": "2cbea4f5fef221d394cb81997f40c7b5a68eff98035644789207181583c9933c" + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayload": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayloadSha256": "530fa7fca53abb9f7f68fcf122fcc0e05ef5bd50b91afb004a2ab9604efd3583" }, { "name": "yanked with a reason after the published status", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, - "signingPayload": "{\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", - "signingPayloadSha256": "23aaf64b36bb65d12bc11e2d59807050107077087dd9e57d9f9d1c2b48ba31a4" + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayload": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayloadSha256": "145b51b5f2e76f6ca40ebb8cd0a29ac8e64eafe092154c23cd6c12956b3388a6" }, { "name": "identical re-fetch of the accepted status", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, - "signingPayload": "{\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", - "signingPayloadSha256": "23aaf64b36bb65d12bc11e2d59807050107077087dd9e57d9f9d1c2b48ba31a4" + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayload": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayloadSha256": "145b51b5f2e76f6ca40ebb8cd0a29ac8e64eafe092154c23cd6c12956b3388a6" }, { "name": "first status of another version of the same tree, tracked separately", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9", + "manifest": "1.2.1 of the same tree", + "now": "2026-10-13T12:00:00Z", "previous": null, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "XMbxiXFkPsciIx6e082vX7myNF6+EwSgoO/uuokUpZk9cgAEMWD3XlOXHZrZxHCCU2eCjWDAfGat/ZoUkqwnCA==" - }, - "signingPayload": "{\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", - "signingPayloadSha256": "6507e4a7925d17dff14749afdc06f714ec034da5e0347cca5d65e843d7ac7498" + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"DjNwD6CfPwAJtAV8fpwCM0xEp/M3Kyed6FuEY7n0YYPF08jUPsmqM8hp+x6Ab6Dt/h/X5AbemtygxbeuuTndCg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayload": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayloadSha256": "4c55983622dc089390e67b3401029e339a0a4028d553df33d54427ca5093fd65" + }, + { + "name": "daily re-sign of the same state", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-15T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "document": "{\"expiresAt\":\"2026-10-16T09:00:00Z\",\"issuedAt\":\"2026-10-15T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":3,\"signature\":\"DOQKzwXbj9ZDHlJ+jtJ1HfIPXtaH3jER4UlbA/ErIpatbihfh/ZH9mlRqi48dJEU41bxmfjXQCNFs7vWobaXCw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayload": "{\"expiresAt\":\"2026-10-16T09:00:00Z\",\"issuedAt\":\"2026-10-15T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":3,\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "signingPayloadSha256": "2fe57bfa8c9cf706f7a30d8d1ecfc14894212a52ec774fd31068240acf2083ec" + }, + { + "name": "yanked release published again", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-15T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "document": "{\"expiresAt\":\"2026-10-16T09:00:00Z\",\"issuedAt\":\"2026-10-15T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":3,\"signature\":\"Pl9KFJ9s/uuN/iUc+kYQolwOWQGxTxI9yxTqwN/8ZERUjalg/S8p4hUX5U5/SGfhP14tDbAcNabPOaGTasHrBg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayload": "{\"expiresAt\":\"2026-10-16T09:00:00Z\",\"issuedAt\":\"2026-10-15T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":3,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayloadSha256": "d85d87cec8314c03433130139e2f468da3715b188c97f2911af0e047ced06eb4" + }, + { + "name": "quarantined to published across a skipped sequence (via yanked)", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-15T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"fSI1jAC48HfiOBo/Ofvfk5rc5VMuZpE1Hu7RHXPswDP+HlWz9NoUl2UkBO/3rABl/tpBpOCmyfMbXZ+uuWbdAQ==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"quarantined\"}", + "document": "{\"expiresAt\":\"2026-10-16T09:00:00Z\",\"issuedAt\":\"2026-10-15T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":4,\"signature\":\"s+AtFBEYn5iDaNEVAZs52mjkjbomoTMzp5YcTNKOK8Sfp8iPP3sYFN9CSBNnhuOG94qTFZboIfWzKxZ8OKmcBw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayload": "{\"expiresAt\":\"2026-10-16T09:00:00Z\",\"issuedAt\":\"2026-10-15T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":4,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayloadSha256": "185bc4059d938d67e7d423e48aa6429428e6dcb7c459dcd613fd15e5b055142c" + }, + { + "name": "checked exactly at expiresAt", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T04:31:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayload": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayloadSha256": "530fa7fca53abb9f7f68fcf122fcc0e05ef5bd50b91afb004a2ab9604efd3583" } ], "invalid": [ { "name": "state changed after signing", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", "expectedCode": "signature.mismatch" }, { "name": "reason changed after signing", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes.", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes.\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", "expectedCode": "signature.mismatch" }, { "name": "sequence raised after signing", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 3, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":3,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "signature.mismatch" + }, + { + "name": "expiry extended after signing", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-21T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", "expectedCode": "signature.mismatch" }, { "name": "signed by another key under the trusted key ID", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "mNutFrw3UlBsoihubIpXLd0uDzsFL53ZMDslc+tkeqg4ygt6104NEn2hRN6OEKqpUG1OLQsXN9cP47mYUUb7Bw==" - }, + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"OSfUAL28TXLpmGkqHYwhmGIU77vAXkaBEkuSE5jN3C9yVFLwstTYKMw3iOMrqG3QgSikwInDU9sJAVKMTyHVDQ==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", "expectedCode": "signature.mismatch" }, { "name": "unknown key ID", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-unknown-key", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-unknown-key\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", "expectedCode": "signature.unknown_key" }, { "name": "signature over an encoding/json-escaped payload", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "OngrStlS3ffBcROhE5/LaiSASqqoR8tDqFNyhZ9/pmJzndXqIwwTTWrXJ58zLwYikmCKLuY7bq55DW9MA01mAw==" - }, + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"nKS2hxRzrYba8CtRTFSSO7aI27KHPkGhOAhtNcQGQCb0l1gwkj+9fXgr8cDxXNiNC+BFvxY0YcRY5ZbRZz73Aw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", "expectedCode": "signature.mismatch" }, { - "name": "lower sequence than the accepted status (rollback)", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" - }, - "expectedCode": "status.rollback" + "name": "pretty-printed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\n \"schemaVersion\": \"skillx.release-status/v1\",\n \"releaseDigest\": \"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\n \"manifestDigest\": \"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\n \"state\": \"yanked\",\n \"reason\": \"Yanked: unsafe script & café notes\",\n \"issuedAt\": \"2026-10-14T09:00:00Z\",\n \"expiresAt\": \"2026-10-15T09:00:00Z\",\n \"sequence\": 2,\n \"keyId\": \"skillx-test-vector-1\",\n \"signatureAlgorithm\": \"ed25519\",\n \"signature\": \"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\"\n}", + "expectedCode": "status.non_canonical" }, { - "name": "same sequence with different content (conflict)", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "1QO+vcKl/Py+KlkINlH1YCFvvlj4C7ZobdaKN8zqONW9vix64W20AfJEH5go7F1glDT46pwN5d+L6JuKIYbSDA==" - }, - "expectedCode": "status.conflict" + "name": "members in insertion order", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"schemaVersion\":\"skillx.release-status/v1\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"state\":\"yanked\",\"reason\":\"Yanked: unsafe script & café notes\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"expiresAt\":\"2026-10-15T09:00:00Z\",\"sequence\":2,\"keyId\":\"skillx-test-vector-1\",\"signatureAlgorithm\":\"ed25519\",\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\"}", + "expectedCode": "status.non_canonical" + }, + { + "name": "duplicate member", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"state\":\"published\",\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "status.non_canonical" + }, + { + "name": "exponent number form", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2e0,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "status.non_canonical" + }, + { + "name": "escaped ASCII character", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed2551\\u0039\",\"state\":\"yanked\"}", + "expectedCode": "status.non_canonical" + }, + { + "name": "byte order mark", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "status.non_canonical" + }, + { + "name": "member differing from a known one only by case, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"State\":\"published\",\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"JjrrNahUVS+3nP7o+xuYy9B2WRljDuiiyBdXyZpeEMVTac56hg8zL0faEDunMa9EW3BhtaKwJaW0S3k85h7gBA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "unknown_field" + }, + { + "name": "empty reason injected, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"DhxVFeMmfwzQ5r2fX7toj3bFGfuPek2Pa9sK0vNnxa6CJbAMtZ+uJmrqWl0gnM4ZkBxFvRA+eX2ACgyQEimsAw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "length" + }, + { + "name": "null reason injected, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":null,\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"MLOwD7Rw21+Gx+OaoyqFVdC3TK+SbVaK5a13j8PFH1bWbPDwhATJ29eji7Cq2ppvUcam47UGTUV0NAs/J3dxDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "type" }, { "name": "unknown state, correctly signed", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "bOkhfaBbnghfh1NtqndPv5jeqPKrdjzzEciLwbGYJrvIolqrEneseTWkAiYpBAUDVJIdgAZbkXd3QdpNvNRWCw==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "revoked", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 3, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "/Fwqm9JZtFZRn/+2juzQKxsmctcsjVkaARtDulsalIS7aKwRSMty/tQUJ0UriJfyL0Uo+y3T4+u3duE/zeqSBg==" - }, + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":3,\"signature\":\"JFNEQS/elLwLb11XInYk+hX+w77CPsqEEp3LasgFctnpPo4aHKybfvWwsvWkJoOhvkCrABJNUQnBrNHMpgWbAg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"revoked\"}", "expectedCode": "status.unknown_state" }, + { + "name": "missing manifestDigest, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"BJtPNJpKFBxvfqtdG6Qb9iPZQ5zaL654Eko3OYSQZwhMBPo2yEAGTzuJ4sH7LMDWfivDXVrzhqOlLodUl3p9Ag==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "required" + }, + { + "name": "missing expiresAt, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", + "previous": null, + "document": "{\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "required" + }, + { + "name": "expiresAt before issuedAt, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-13T04:30:59Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"O+gZkw8lHhtJ7zefbstOGSXJPPe4cYsPHeIrWN5h16hGotkWYGChChtaxYFBwNYPiICICDImGvadjASh/nnECg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "status.invalid_lifetime" + }, + { + "name": "lifetime longer than 7 days, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-20T04:31:01Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"WUlp2TnLSkmeQHFiv3Jok2AEU4bPJO+5PWjITX8SO8JCIXq9vLKThLZ+jRdF/cFfoW4JhV9wxSFYZt+0yPc7Bw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "status.invalid_lifetime" + }, + { + "name": "first status that is yanked, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"PWR/QEPHvJRut0Gnwjo948jiFrZFvXKyC5eNPDittTL3kXLZwXeP4QeSwYqgn++JkBHK3QEqio+Ed0Xi7pvYDw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "status.invalid_transition" + }, + { + "name": "reason over 512 UTF-8 bytes, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé.\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"ll+/hBkM2OBu/lGLyIc9PqRcmh6P80kVqSriBjA40iMgcUzLKm3qT1Cd/oOUo/8lgCcY0OBDOIb0qihIxkbdDQ==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "length" + }, + { + "name": "bidi override in the reason, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"see ‮hs.kcehc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"qN86bLtTH7GNNgJwmx/3SKzdbvBW8rKxDGPGbRnzy1Dj433gyBHLsQTxuGh/zc81lzwPfHqBCgzBlJCHDbRyBw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "format" + }, { "name": "status of another release tree", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", "previous": null, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544", - "manifestDigest": "sha256:5979df5f8e7c400ac422183440040742bb34274bca37c9347d100948871e3596", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "ZOZz9Gji6upxt6uIOH5RBfOHBnDhI7SXCEcXyqLhDzO0a+XkM9O+DecmW++ATHoYEB1KW8pt+4nbdVqmWi9qBw==" - }, + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:5979df5f8e7c400ac422183440040742bb34274bca37c9347d100948871e3596\",\"releaseDigest\":\"sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"vjp2IFJqOHK+v0xHvQSHnXTZZYcjYbLxH9QQB5e4O1bfZ1OkIWCUWt0AL8pjXJ+L6Fci0DjBdhsATDth7HCxBg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", "expectedCode": "status.digest_mismatch" }, { "name": "status of another version of the same tree", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "previous": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "AUULhdLU6L0sCQXUi7CXFISvhwvyF27XrIlW7Z1VVdeYLOLFGaS238Njby+XFXkgHCTKvHJe62Y+ZvelnPtTBQ==" - }, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9", - "state": "yanked", - "reason": "Yanked: unsafe script & café notes", - "issuedAt": "2026-10-14T09:00:00Z", - "sequence": 2, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "rKAqPl+D+SOgMhSvX8FKHZ2VmUvAxilg744OKej/gGrpyvuHf8yYHui4sQ5+GBCrjpHKUG3Z6284vAwlVDW7BA==" - }, + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:c598df8848000fea906e7e4d60237c30d927ef4ee26e5148b9091920eb0016a9\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"GCa4fNHeaF4H9SkeRXVZja8UWPtbQeMSLC4R8txof1gFfLY7G666mrneli0STnizR53dxfcSMs2bxnUxcUoODA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", "expectedCode": "status.manifest_mismatch" }, { - "name": "missing manifestDigest, correctly signed", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", + "name": "wrong manifestDigest, correctly signed", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", "previous": null, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "Lt3PlElJm6IihJxCgc3V1Md3UnkrNEiBle7RpIHLmG6iuoEp8sJRVvG7lz6ZdT0OLwYW8VkaTIJTtqnx+KyBDA==" - }, - "expectedCode": "required" + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"RZmIudxDeH0v5lTPc2EYQM4u/wQupwR2gaJF7KrNkyneWQyjCKyNMOEj1lopIuOGFSPZm0TldmHoOIq6peafCw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "status.manifest_mismatch" }, { - "name": "wrong manifestDigest, correctly signed", - "expectedReleaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "expectedManifestDigest": "sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc", + "name": "issued before the manifest was created", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", "previous": null, - "status": { - "schemaVersion": "skillx.release-status/v1", - "releaseDigest": "sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974", - "manifestDigest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "state": "published", - "issuedAt": "2026-10-13T04:31:00Z", - "sequence": 1, - "keyId": "skillx-test-vector-1", - "signatureAlgorithm": "ed25519", - "signature": "pD8ZqKLe0csnXK+uN46nAzwEb9K0O5qnpK1qiLS9cXOKEnx9jYPu4FCBi2nbiz/MwTUfPzQ+QRQjv/znoSz0DA==" - }, - "expectedCode": "status.manifest_mismatch" + "document": "{\"expiresAt\":\"2026-10-14T04:29:59Z\",\"issuedAt\":\"2026-10-13T04:29:59Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"9ovE7/Rbq/dIFL5Aigbagfzb9BUbLT6+gVz8R4mdSyhtwxcl7IFj+RLxkpl9K3RvfpV6yPu9TZmFBdcOYc2oCg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "status.issued_before_release" + }, + { + "name": "expired one second after expiresAt", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T04:31:01Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "status.expired" + }, + { + "name": "lower sequence than the accepted status (rollback)", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "status.rollback" + }, + { + "name": "same sequence with different content (conflict)", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"yBF2lq3npr1NOJaMNM1xinpOh9RBX/9uy4ahiJHobRkl3ZRgNzir0zkagSrVl+NnPlRILNi/II1BjvI26dcGBw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "status.conflict" + }, + { + "name": "higher sequence issued no later (issuedAt regression)", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":3,\"signature\":\"YFTjuOtuMygvouP2+R54t6NtQVSI6iAqxTvc6txDjvD5ZNzXyXCs/z6tbyV03HYR+n8lRwtHFKpPw+3UEbpiDA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "status.issued_at_regression" + }, + { + "name": "yanked back to draft", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-15T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABA==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "document": "{\"expiresAt\":\"2026-10-16T09:00:00Z\",\"issuedAt\":\"2026-10-15T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":3,\"signature\":\"gHc+lCAgq8O5/xxaceuBBSKwjUbPNr8UL9aG0I/Xful+LfLQgS2d4a4NAGuX5aXnSHBi/DrSvmBi1rPGrSUbBw==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"draft\"}", + "expectedCode": "status.invalid_transition" + }, + { + "name": "quarantined straight to published", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-15T12:00:00Z", + "previous": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"fSI1jAC48HfiOBo/Ofvfk5rc5VMuZpE1Hu7RHXPswDP+HlWz9NoUl2UkBO/3rABl/tpBpOCmyfMbXZ+uuWbdAQ==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"quarantined\"}", + "document": "{\"expiresAt\":\"2026-10-16T09:00:00Z\",\"issuedAt\":\"2026-10-15T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":3,\"signature\":\"Pl9KFJ9s/uuN/iUc+kYQolwOWQGxTxI9yxTqwN/8ZERUjalg/S8p4hUX5U5/SGfhP14tDbAcNabPOaGTasHrBg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "status.invalid_transition" } ] } diff --git a/packages/contracts/src/package-manifest.test.ts b/packages/contracts/src/package-manifest.test.ts index 6cb03ca..d2a4906 100644 --- a/packages/contracts/src/package-manifest.test.ts +++ b/packages/contracts/src/package-manifest.test.ts @@ -6,10 +6,11 @@ import { INGESTION_FIXTURES, PACKAGE_MANIFEST_FIXTURES, PACKAGE_MANIFEST_FIXTURE, - RELEASE_MANIFEST_FIXTURES, - RELEASE_STATUS_FIXTURES, VERIFICATION_EVIDENCE_FIXTURES, } from "./fixtures"; +import { RELEASE_MANIFEST_FIXTURES } from "./fixtures/release-fixtures"; +import { RELEASE_STATUS_FIXTURES } from "./fixtures/release-status-fixtures"; +import * as contracts from "./index"; import { validateReleaseManifest } from "./release-manifest"; import { validateReleaseStatus } from "./release-status"; import { @@ -36,18 +37,27 @@ const VALIDATOR_BY_GROUP: Record { valid: boolean; i "release-status": validateReleaseStatus, }; +/** Every fixture case, including the test-only signed-release fixtures the root export leaves out. */ +const EVERY_FIXTURE = [...ALL_FIXTURES, ...RELEASE_MANIFEST_FIXTURES, ...RELEASE_STATUS_FIXTURES]; + describe("skillx.package/v1 fixtures", () => { it("covers both a valid and a rejected case for every contract group", () => { - const groups = new Set(ALL_FIXTURES.map((fixture) => fixture.group)); + const groups = new Set(EVERY_FIXTURE.map((fixture) => fixture.group)); expect(groups.size).toBe(8); for (const group of groups) { - const cases = ALL_FIXTURES.filter((fixture) => fixture.group === group); + const cases = EVERY_FIXTURE.filter((fixture) => fixture.group === group); expect(cases.some((c) => c.expectedValid), `${group} needs a valid fixture`).toBe(true); expect(cases.some((c) => !c.expectedValid), `${group} needs a rejected fixture`).toBe(true); } }); - it.each(ALL_FIXTURES)("$group :: $name is valid=$expectedValid", ({ group, value, expectedValid, expectedIssueCode }) => { + it("keeps the test-only signed-release fixtures out of the package root", () => { + expect("RELEASE_MANIFEST_FIXTURES" in contracts).toBe(false); + expect("RELEASE_STATUS_FIXTURES" in contracts).toBe(false); + expect(ALL_FIXTURES.some((f) => f.group === "release-manifest" || f.group === "release-status")).toBe(false); + }); + + it.each(EVERY_FIXTURE)("$group :: $name is valid=$expectedValid", ({ group, value, expectedValid, expectedIssueCode }) => { const result = VALIDATOR_BY_GROUP[group]?.(value); expect(result, `no validator registered for group ${group}`).toBeDefined(); expect(result?.valid).toBe(expectedValid); @@ -69,9 +79,7 @@ describe("skillx.package/v1 fixtures", () => { VERIFICATION_EVIDENCE_FIXTURES.length + COLLECTION_MEMBER_FIXTURES.length + ENTITLEMENT_FIXTURES.length + - INGESTION_FIXTURES.length + - RELEASE_MANIFEST_FIXTURES.length + - RELEASE_STATUS_FIXTURES.length, + INGESTION_FIXTURES.length, ); }); }); diff --git a/packages/contracts/src/release-manifest.test.ts b/packages/contracts/src/release-manifest.test.ts index b45d55a..3939098 100644 --- a/packages/contracts/src/release-manifest.test.ts +++ b/packages/contracts/src/release-manifest.test.ts @@ -1,16 +1,21 @@ import { describe, expect, it } from "vitest"; +import { canonicalJson } from "./canonical-json"; import { RELEASE_FIXTURE_KEY_ID, RELEASE_FIXTURE_UNICODE_PATH, RELEASE_MANIFEST_FIXTURE, RELEASE_MANIFEST_MINIMAL_FIXTURE, signedReleaseDraft, -} from "./fixtures"; +} from "./fixtures/release-fixtures"; import { + RELEASE_MANIFEST_CODES, RELEASE_SIGNATURE_CODES, + RELEASE_SIGNATURE_PATTERN, + RELEASE_VERSION_PATTERN, ReleaseManifestError, compareUtf8, releaseSigningPayload, + releaseTreeDigest, signReleaseManifest, validateReleaseManifest, validateUnsignedReleaseManifest, @@ -19,6 +24,7 @@ import { import type { SkillxReleaseManifest, UnsignedReleaseManifest } from "./release-manifest"; const decoder = new TextDecoder(); +const doc = (value: unknown): string => canonicalJson(value); async function generateKey(): Promise<{ privateKey: CryptoKey; publicKey: Uint8Array }> { const pair = (await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"])) as CryptoKeyPair; @@ -60,12 +66,48 @@ describe("skillx.release/v1 validation", () => { expect(result.issues).toContainEqual(expect.objectContaining({ path: "signature", code: "unknown_field" })); }); - it("rejects non-object input without throwing", () => { - for (const input of [null, undefined, "skillx.release/v1", [], 7]) { + it("rejects any non-document input without throwing", () => { + // eslint-disable-next-line no-sparse-arrays + const sparseFiles = [, RELEASE_MANIFEST_FIXTURE.files[0]]; + const inputs: unknown[] = [ + null, + undefined, + "skillx.release/v1", + [], + 7, + new Date(0), + new Map(), + Object.create(null), + { ...signedReleaseDraft(), files: sparseFiles }, + { ...signedReleaseDraft(), files: [{ ...RELEASE_MANIFEST_FIXTURE.files[0], path: new String("SKILL.md") }] }, + { ...signedReleaseDraft(), artifactSize: 10240n }, + { ...signedReleaseDraft(), artifactSize: Number.POSITIVE_INFINITY }, + { ...signedReleaseDraft(), identity: `@publisher/${String.fromCharCode(0xdc00)}` }, + ]; + for (const input of inputs) { + expect(() => validateReleaseManifest(input)).not.toThrow(); expect(validateReleaseManifest(input).valid).toBe(false); } }); + it("accepts SemVer 2.0 versions without build metadata or leading zeros", () => { + for (const version of ["0.0.0", "1.2.3", "10.20.30", "1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-0.3.7", "1.0.0-x-y.z--", "1.0.0-0a"]) { + expect(RELEASE_VERSION_PATTERN.test(version), version).toBe(true); + } + for (const version of ["1.2", "v1.2.3", "01.2.3", "1.02.3", "1.2.03", "1.2.3-01", "1.2.3+build", "1.2.3-", "1.2.3-a..b", "1.2.3-a_b"]) { + expect(RELEASE_VERSION_PATTERN.test(version), version).toBe(false); + } + const at = (version: string) => ({ ...RELEASE_MANIFEST_FIXTURE, version }); + expect(validateUnsignedReleaseManifest(at(`1.2.0-${"a".repeat(122)}`)).valid).toBe(true); + expect(validateUnsignedReleaseManifest(at(`1.2.0-${"a".repeat(123)}`)).issues.map((i) => i.path)).toEqual(["version"]); + }); + + it("accepts only the one canonical base64 text of a 64-byte signature", () => { + expect(RELEASE_SIGNATURE_PATTERN.test(`${"A".repeat(85)}w==`)).toBe(true); + expect(RELEASE_SIGNATURE_PATTERN.test(`${"A".repeat(85)}B==`)).toBe(false); + expect(RELEASE_SIGNATURE_PATTERN.test(`${"A".repeat(84)}AA=`)).toBe(false); + }); + it("reports every missing field of an empty object", () => { const paths = validateReleaseManifest({}).issues.filter((i) => i.code === "required").map((i) => i.path); expect(paths).toContain("releaseDigest"); @@ -131,36 +173,68 @@ describe("skillx.release/v1 signing payload", () => { }); }); +describe("skillx.release/v1 tree digest", () => { + it("recomputes the fixture release digests with WebCrypto", async () => { + expect(await releaseTreeDigest(RELEASE_MANIFEST_FIXTURE.files)).toBe(RELEASE_MANIFEST_FIXTURE.releaseDigest); + expect(await releaseTreeDigest(RELEASE_MANIFEST_MINIMAL_FIXTURE.files)).toBe(RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest); + }); + + it("refuses to sign a manifest whose releaseDigest is not the tree digest of its files", async () => { + const { privateKey } = await generateKey(); + const wrong = { ...RELEASE_MANIFEST_FIXTURE, releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest }; + const error = await signReleaseManifest(wrong, privateKey).catch((e: unknown) => e); + expect(error).toBeInstanceOf(ReleaseManifestError); + expect((error as ReleaseManifestError).issues.map((i) => i.code)).toEqual([RELEASE_MANIFEST_CODES.treeDigestMismatch]); + }); +}); + describe("skillx.release/v1 signatures", () => { - it("verifies a manifest signed with the trusted key", async () => { + it("verifies a manifest signed with the trusted key, from text or bytes", async () => { const { privateKey, publicKey } = await generateKey(); const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); const keys = new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]]); - expect(await verifyReleaseManifest(signed, keys)).toEqual({ valid: true, issues: [] }); - expect(await verifyReleaseManifest(reverseKeys(signed), keys)).toEqual({ valid: true, issues: [] }); + expect(await verifyReleaseManifest(doc(signed), keys)).toEqual({ valid: true, issues: [] }); + expect(await verifyReleaseManifest(new TextEncoder().encode(doc(signed)), keys)).toEqual({ valid: true, issues: [] }); + }); + + it.each<[string, (text: string) => string]>([ + ["reordered members", (t) => JSON.stringify(reverseKeys(JSON.parse(t)))], + ["pretty-printed", (t) => JSON.stringify(JSON.parse(t), null, 2)], + ["a duplicate member", (t) => t.replace("{", '{"version":"9.9.9",')], + ["an exponent number form", (t) => t.replace('"artifactSize":10240', '"artifactSize":1.024e4')], + ["an escaped slash", (t) => t.replace("scripts/check.sh", "scripts\\/check.sh")], + ])("rejects %s as non-canonical", async (_name, mangle) => { + const { privateKey, publicKey } = await generateKey(); + const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); + const result = await verifyReleaseManifest(mangle(doc(signed)), new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]])); + expect(result.issues.map((i) => i.code)).toEqual([RELEASE_MANIFEST_CODES.nonCanonical]); }); - it.each<[string, (m: SkillxReleaseManifest) => void]>([ - ["a changed version", (m) => void (m.version = "1.2.1")], - ["a changed file hash", (m) => void (m.files[0]!.sha256 = `${m.files[0]!.sha256.slice(0, 63)}0`)], - ["a changed release digest", (m) => void (m.releaseDigest = `sha256:${"e".repeat(64)}`)], - ["a changed publisher verification", (m) => void (m.publisher.verifiedBy = "admin")], - ["a flipped signature byte", (m) => void (m.signature = flipBase64Byte(m.signature))], - ])("rejects %s", async (_name, tamper) => { + it.each<[string, (m: SkillxReleaseManifest) => void, string]>([ + ["a changed version", (m) => void (m.version = "1.2.1"), RELEASE_SIGNATURE_CODES.mismatch], + [ + "a changed file hash", + (m) => void (m.files[0]!.sha256 = `${m.files[0]!.sha256.slice(0, 63)}0`), + RELEASE_MANIFEST_CODES.treeDigestMismatch, + ], + ["a changed release digest", (m) => void (m.releaseDigest = `sha256:${"e".repeat(64)}`), RELEASE_MANIFEST_CODES.treeDigestMismatch], + ["a changed publisher verification", (m) => void (m.publisher.verifiedBy = "admin"), RELEASE_SIGNATURE_CODES.mismatch], + ["a flipped signature byte", (m) => void (m.signature = flipBase64Byte(m.signature)), RELEASE_SIGNATURE_CODES.mismatch], + ])("rejects %s", async (_name, tamper, code) => { const { privateKey, publicKey } = await generateKey(); const signed = clone(await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey)); tamper(signed); expect(validateReleaseManifest(signed).valid).toBe(true); - const result = await verifyReleaseManifest(signed, new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]])); + const result = await verifyReleaseManifest(doc(signed), new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]])); expect(result.valid).toBe(false); - expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + expect(result.issues.map((i) => i.code)).toEqual([code]); }); it("rejects a signature from another key under the trusted key ID", async () => { const signer = await generateKey(); const trusted = await generateKey(); const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, signer.privateKey); - const result = await verifyReleaseManifest(signed, new Map([[RELEASE_FIXTURE_KEY_ID, trusted.publicKey]])); + const result = await verifyReleaseManifest(doc(signed), new Map([[RELEASE_FIXTURE_KEY_ID, trusted.publicKey]])); expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); }); @@ -174,27 +248,27 @@ describe("skillx.release/v1 signatures", () => { ["skillx-other-key", a.publicKey], ["skillx-third-key", b.publicKey], ]); - const result = await verifyReleaseManifest(signed, keys); + const result = await verifyReleaseManifest(doc(signed), keys); expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); }); it("rejects an unknown key ID", async () => { const { privateKey, publicKey } = await generateKey(); const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); - const result = await verifyReleaseManifest(signed, new Map([["skillx-other-key", publicKey]])); + const result = await verifyReleaseManifest(doc(signed), new Map([["skillx-other-key", publicKey]])); expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.unknownKey]); }); it("rejects a malformed public key without throwing", async () => { const { privateKey } = await generateKey(); const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); - const result = await verifyReleaseManifest(signed, new Map([[RELEASE_FIXTURE_KEY_ID, new Uint8Array(31)]])); + const result = await verifyReleaseManifest(doc(signed), new Map([[RELEASE_FIXTURE_KEY_ID, new Uint8Array(31)]])); expect(result.issues.map((i) => i.code)).toEqual([RELEASE_SIGNATURE_CODES.invalidKey]); }); it("returns structural issues for an invalid manifest instead of verifying", async () => { const { publicKey } = await generateKey(); - const result = await verifyReleaseManifest({ schemaVersion: "skillx.release/v1" }, new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]])); + const result = await verifyReleaseManifest(doc({ schemaVersion: "skillx.release/v1" }), new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]])); expect(result.valid).toBe(false); expect(result.issues.map((i) => i.code)).not.toContain(RELEASE_SIGNATURE_CODES.mismatch); }); diff --git a/packages/contracts/src/release-manifest.ts b/packages/contracts/src/release-manifest.ts index 0bacfec..adb35f2 100644 --- a/packages/contracts/src/release-manifest.ts +++ b/packages/contracts/src/release-manifest.ts @@ -8,21 +8,34 @@ * never changes; the mutable release state lives in the separately signed * `skillx.release-status/v1` document (`release-status.ts`). Signature rules * are in `release-signature.ts`: Ed25519 over the canonical JSON of every - * member except `signature`. + * member except `signature`. `releaseDigest` must equal the tree digest of + * `files`; signing and verification both enforce it. * - * Validators never throw on malformed input. + * Validators and verifiers never throw, whatever the input. */ -import { ERROR_CODES, SkillTreeError, checkCollisions, checkPublisher, isNoisePath, validatePath } from "@skillx/skill-tree/rules"; +import { + ERROR_CODES, + SkillTreeError, + checkCollisions, + checkPublisher, + compareUtf8, + isNoisePath, + serializeTree, + validatePath, +} from "@skillx/skill-tree/rules"; import { canonicalJsonBytes } from "./canonical-json"; -import { PACKAGE_NAME_PATTERN, RELEASE_CHANNELS, SEMVER_PATTERN, isPackageKind, PACKAGE_KINDS } from "./package-manifest"; +import { PACKAGE_NAME_PATTERN, RELEASE_CHANNELS, isPackageKind, PACKAGE_KINDS } from "./package-manifest"; import type { PackageKind, ReleaseChannel } from "./package-manifest"; import { RELEASE_SIGNATURE_ALGORITHM, ReleaseManifestError, + checkCanonicalValue, checkFields as checkSchemaFields, checkSignatureMembers, isReleaseTimestamp, + parseReceivedDocument, + sha256Digest, signPayload, verifyPayload, } from "./release-signature"; @@ -40,16 +53,36 @@ export { ReleaseManifestError, } from "./release-signature"; export type { ReleasePublicKeys } from "./release-signature"; +export { compareUtf8 } from "@skillx/skill-tree/rules"; export const SKILLX_RELEASE_SCHEMA_VERSION = "skillx.release/v1"; +/** Verification failure codes of the release manifest itself. */ +export const RELEASE_MANIFEST_CODES = { + /** The received bytes are not exactly the canonical JSON of the document. */ + nonCanonical: "release.non_canonical", + /** `releaseDigest` is not the `agentkit.skill-tree/v1` digest of `files`. */ + treeDigestMismatch: "release.tree_digest_mismatch", +} as const; + +/** + * SemVer 2.0 version of a release: no build metadata (`+...` has no + * precedence, so two releases could not be ordered) and no leading zeros in + * numeric identifiers. Stricter than the package-wide `SEMVER_PATTERN`. + */ +export const RELEASE_VERSION_PATTERN = + /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?$/; +const MAX_RELEASE_VERSION_LENGTH = 128; + /** Mode classes of `agentkit.skill-tree/v1`. */ export const RELEASE_FILE_MODES = ["100644", "100755"] as const; export type ReleaseFileMode = (typeof RELEASE_FILE_MODES)[number]; /** - * How SkillX verified that the publisher owns its handle. `none` means the - * release is not publisher-verified, so AgentKit trusts it as `local-unverified`. + * How SkillX verified that the publisher owns its handle. The signature is a + * registry key, never a publisher key: AgentKit maps a verified registry + * signature with `verifiedBy != "none"` to trust `registry-signed` (not + * `publisher-signed`), and `none` to `local-unverified`. */ export const PUBLISHER_VERIFICATIONS = ["none", "github", "admin"] as const; export type PublisherVerification = (typeof PUBLISHER_VERIFICATIONS)[number]; @@ -144,19 +177,6 @@ const FILE_FIELDS = ["path", "mode", "sha256", "size"] as const; const PUBLISHER_FIELDS = ["handle", "verifiedBy"] as const; const PROVENANCE_FIELDS = ["repo", "commit", "path"] as const; -/** Compares well-formed strings by UTF-8 bytes, which is code point order. */ -export function compareUtf8(a: string, b: string): number { - const ia = a[Symbol.iterator](); - const ib = b[Symbol.iterator](); - for (;;) { - const ca = ia.next(); - const cb = ib.next(); - if (ca.done || cb.done) return ca.done && cb.done ? 0 : ca.done ? -1 : 1; - const diff = ca.value.codePointAt(0)! - cb.value.codePointAt(0)!; - if (diff !== 0) return diff < 0 ? -1 : 1; - } -} - const isSize = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0; function oneOf(values: readonly T[], value: unknown): value is T { @@ -312,14 +332,25 @@ function validateDeclaredScripts(scripts: unknown, files: ReleaseFile[] | null, function collectIssues(input: unknown, requireSignature: boolean): ValidationIssue[] { if (!isPlainObject(input)) return [issue("", "type", "release manifest must be an object")]; const issues: ValidationIssue[] = []; + if (!checkCanonicalValue(input, issues)) return issues; checkFields(input, requireSignature ? [...MANIFEST_FIELDS, "signature"] : MANIFEST_FIELDS, "", issues); if (input.schemaVersion !== SKILLX_RELEASE_SCHEMA_VERSION) { issues.push(issue("schemaVersion", "unsupported_value", `must equal '${SKILLX_RELEASE_SCHEMA_VERSION}'`)); } const handle = validateIdentityField(input.identity, issues); - if (typeof input.version !== "string" || !SEMVER_PATTERN.test(input.version)) { - issues.push(issue("version", "pattern", "must be a semantic version such as '0.1.0'")); + if ( + typeof input.version !== "string" || + input.version.length > MAX_RELEASE_VERSION_LENGTH || + !RELEASE_VERSION_PATTERN.test(input.version) + ) { + issues.push( + issue( + "version", + "pattern", + `must be a SemVer 2.0 version without build metadata or leading zeros, at most ${MAX_RELEASE_VERSION_LENGTH} characters, such as '0.1.0'`, + ), + ); } if (!oneOf(RELEASE_CHANNELS, input.channel)) { issues.push(issue("channel", "unsupported_value", `must be one of: ${RELEASE_CHANNELS.join(", ")}`)); @@ -390,14 +421,30 @@ export function releaseSigningPayload(manifest: UnsignedReleaseManifest | Skillx * under different identities or versions have different manifest digests. */ export async function releaseManifestDigest(manifest: UnsignedReleaseManifest | SkillxReleaseManifest): Promise { - const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", releaseSigningPayload(manifest))); - return `sha256:${Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("")}`; + return sha256Digest(releaseSigningPayload(manifest)); +} + +/** + * The `agentkit.skill-tree/v1` digest of `files`, computed with WebCrypto over + * the pure serialization of `@skillx/skill-tree/rules`. Equal to `treeDigest` + * of `@skillx/skill-tree` for the same files. + */ +export async function releaseTreeDigest(files: readonly ReleaseFile[]): Promise { + return sha256Digest(new TextEncoder().encode(serializeTree(files))); +} + +async function treeDigestIssue(manifest: UnsignedReleaseManifest): Promise { + const digest = await releaseTreeDigest(manifest.files); + return digest === manifest.releaseDigest + ? null + : issue("releaseDigest", RELEASE_MANIFEST_CODES.treeDigestMismatch, `is not the tree digest of files (${digest})`); } /** * Signs a valid unsigned manifest with an Ed25519 private key. The key ID and * algorithm are part of the signed payload. Throws `ReleaseManifestError` when - * the manifest is invalid or the key cannot sign with Ed25519. + * the manifest is invalid, its `releaseDigest` is not the tree digest of its + * `files`, or the key cannot sign with Ed25519. */ export async function signReleaseManifest( manifest: UnsignedReleaseManifest, @@ -405,18 +452,30 @@ export async function signReleaseManifest( ): Promise { const result = validateUnsignedReleaseManifest(manifest); if (!result.valid) throw new ReleaseManifestError("invalid release manifest", result.issues); + const mismatch = await treeDigestIssue(manifest); + if (mismatch !== null) throw new ReleaseManifestError("invalid release manifest", [mismatch]); return { ...manifest, signature: await signPayload(releaseSigningPayload(manifest), privateKey) }; } /** - * Validates a signed manifest and verifies its signature against `publicKeys`. - * Fails closed: an invalid manifest, unknown key ID, malformed key or bad - * signature is an invalid result, never an exception. + * Verifies a received manifest document against `publicKeys`, in this order: + * the bytes are exactly its canonical JSON (`release.non_canonical`), the + * structure is valid, `releaseDigest` is the tree digest of `files` + * (`release.tree_digest_mismatch`), and the signature verifies. Fails closed: + * every failure is an invalid result, never an exception. The verified + * manifest is the parsed `document`. */ -export async function verifyReleaseManifest(input: unknown, publicKeys: ReleasePublicKeys): Promise { - const structure = validateReleaseManifest(input); +export async function verifyReleaseManifest( + document: string | Uint8Array, + publicKeys: ReleasePublicKeys, +): Promise { + const parsed = parseReceivedDocument(document, RELEASE_MANIFEST_CODES.nonCanonical); + if (!("value" in parsed)) return parsed; + const structure = validateReleaseManifest(parsed.value); if (!structure.valid) return structure; - const manifest = input as SkillxReleaseManifest; + const manifest = parsed.value as SkillxReleaseManifest; + const mismatch = await treeDigestIssue(manifest); + if (mismatch !== null) return validationFail([mismatch]); const failure = await verifyPayload(releaseSigningPayload(manifest), manifest.signature, manifest.keyId, publicKeys); return failure === null ? validationOk() : validationFail([failure]); } diff --git a/packages/contracts/src/release-signature.ts b/packages/contracts/src/release-signature.ts index 496b806..30c4c71 100644 --- a/packages/contracts/src/release-signature.ts +++ b/packages/contracts/src/release-signature.ts @@ -7,18 +7,58 @@ * `signatureAlgorithm` are signed too. The signature is the standard (padded) * base64 of the 64-byte Ed25519 signature, as in AgentKit's remote registry * manifests. WebCrypto `Ed25519` works in Workers, browsers and Node.js. + * + * Domain separation: there is no prefix before the payload. Every signed + * payload holds its own `schemaVersion`, every verifier requires its one + * schema version and rejects unknown members, and registry release keys sign + * nothing but these two schemas, so a signature over one document type never + * verifies as the other. + * + * Served documents are exactly `canonicalJson(document)`, signature included; + * verifiers parse with `parseCanonicalJson`, which rejects any other bytes. */ -import { issue } from "./validation"; -import type { ValidationIssue } from "./validation"; +import { CanonicalJsonError, canonicalJson, parseCanonicalJson } from "./canonical-json"; +import { issue, validationFail } from "./validation"; +import type { ValidationIssue, ValidationResult } from "./validation"; export const RELEASE_SIGNATURE_ALGORITHM = "ed25519"; /** Same key ID rule as AgentKit remote registry manifests. */ export const RELEASE_KEY_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{1,63}$/; /** RFC 3339 UTC timestamp with whole seconds, e.g. `2026-10-13T04:30:00Z`. */ export const RELEASE_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; -/** Padded standard base64 of a 64-byte Ed25519 signature. */ -export const RELEASE_SIGNATURE_PATTERN = /^[A-Za-z0-9+/]{86}==$/; +/** + * Padded standard base64 of a 64-byte Ed25519 signature. The last data + * character carries 4 unused bits that must be zero, so each signature has + * exactly one accepted text. + */ +export const RELEASE_SIGNATURE_PATTERN = /^[A-Za-z0-9+/]{85}[AQgw]==$/; + +/** + * Checks that `input` lies in the canonical JSON value space (plain objects, + * dense arrays, non-negative safe integers, well-formed strings). Validators + * call this first so that no later step can throw on programmatic input. + */ +export function checkCanonicalValue(input: unknown, issues: ValidationIssue[]): boolean { + try { + canonicalJson(input); + return true; + } catch (error) { + if (!(error instanceof CanonicalJsonError)) throw error; + issues.push(issue("", "type", `is outside the canonical JSON value space: ${error.message}`)); + return false; + } +} + +/** + * Parses received document bytes for a verifier. Returns the value, or the + * failed result with `code` (e.g. `release.non_canonical`) when the bytes are + * not exactly the canonical JSON of their value. + */ +export function parseReceivedDocument(input: string | Uint8Array, code: string): { value: unknown } | ValidationResult { + const parsed = parseCanonicalJson(input); + return parsed.ok ? { value: parsed.value } : validationFail([issue("", code, `document ${parsed.reason}`)]); +} /** Signature verification failure codes. */ export const RELEASE_SIGNATURE_CODES = { @@ -53,6 +93,17 @@ export function checkFields( } } +/** `sha256:<64 lowercase hex>` of `bytes`, computed with WebCrypto. */ +export async function sha256Digest(bytes: Uint8Array): Promise { + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); + return `sha256:${Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("")}`; +} + +/** Milliseconds since the epoch of a valid release timestamp. */ +export function timestampMs(value: string): number { + return Date.parse(value); +} + export function isReleaseTimestamp(value: unknown): boolean { if (typeof value !== "string" || !RELEASE_TIMESTAMP_PATTERN.test(value)) return false; const ms = Date.parse(value); diff --git a/packages/contracts/src/release-status-vectors.test.ts b/packages/contracts/src/release-status-vectors.test.ts index 6878b84..98a812b 100644 --- a/packages/contracts/src/release-status-vectors.test.ts +++ b/packages/contracts/src/release-status-vectors.test.ts @@ -1,19 +1,24 @@ import { describe, expect, it } from "vitest"; +import { canonicalJson } from "./canonical-json"; +import { RELEASE_FIXTURE_KEY_ID, RELEASE_MANIFEST_FIXTURE, RELEASE_MANIFEST_MINIMAL_FIXTURE } from "./fixtures/release-fixtures"; import { - RELEASE_FIXTURE_KEY_ID, RELEASE_FIXTURE_MANIFEST_DIGEST, - RELEASE_MANIFEST_FIXTURE, - RELEASE_MANIFEST_MINIMAL_FIXTURE, RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST, RELEASE_STATUS_FIXTURE_REASON, RELEASE_STATUS_PUBLISHED_FIXTURE, RELEASE_STATUS_YANKED_FIXTURE, -} from "./fixtures"; +} from "./fixtures/release-status-fixtures"; import vectorFile from "./fixtures/release-status-v1.json"; import { RELEASE_SIGNATURE_CODES, releaseManifestDigest, signReleaseManifest, verifyReleaseManifest } from "./release-manifest"; import type { SkillxReleaseManifest } from "./release-manifest"; import { signPayload } from "./release-signature"; -import { RELEASE_STATUS_CODES, releaseStatusSigningPayload, signReleaseStatus, verifyReleaseStatus } from "./release-status"; +import { + RELEASE_STATUS_CODES, + RELEASE_STATUS_MAX_LIFETIME_SECONDS, + RELEASE_STATUS_REASON_MAX_BYTES, + releaseStatusSigningPayload, + verifyReleaseStatus, +} from "./release-status"; import type { SkillxReleaseStatus, UnsignedReleaseStatus } from "./release-status"; import { OTHER_VECTOR_KEY_SEED_TEXT, @@ -35,10 +40,11 @@ import type { VectorKey, VectorKeyDescription } from "./test-support/vector-key" /** * Golden cross-language vectors for `skillx.release-status/v1`, exported for * AgentKit (`fixtures/release-status-v1.json`). Same public test key as the - * manifest vectors. `manifests` lists the signed manifests the statuses refer - * to with their manifest digests; each vector names the release and manifest - * digests the consumer expects and the status it accepted before for that - * manifest (`previous`, or `null`). + * manifest vectors. `manifests` lists the signed manifest documents the + * statuses refer to with their manifest digests. Each vector names the + * verified manifest it is checked against (`manifest`), the consumer's clock + * (`now`), the status document it accepted before for that manifest + * (`previous`, or `null`) and the received `document`. * * Regenerate after an intentional contract change with * `SKILLX_WRITE_RELEASE_VECTORS=1 pnpm vitest run packages/contracts/src`. @@ -46,10 +52,10 @@ import type { VectorKey, VectorKeyDescription } from "./test-support/vector-key" interface StatusVector { name: string; - expectedReleaseDigest: string; - expectedManifestDigest: string; - previous: SkillxReleaseStatus | null; - status: SkillxReleaseStatus; + manifest: string; + now: string; + previous: string | null; + document: string; } interface StatusVectors { @@ -58,12 +64,21 @@ interface StatusVectors { manifestDigestRule: string; consumerRules: string[]; testKey: VectorKeyDescription; - manifests: Array<{ name: string; manifest: SkillxReleaseManifest; manifestDigest: string }>; + manifests: Array<{ name: string; document: string; manifestDigest: string }>; valid: Array; invalid: Array; } -/** Signs any status bytes, including ones `signReleaseStatus` refuses (e.g. an unknown state). */ +const V120 = "1.2.0 of the multi-file tree"; +const V121 = "1.2.1 of the same tree"; +const MINIMAL = "minimal release"; +const DAY_ONE = "2026-10-13T12:00:00Z"; +const DAY_TWO = "2026-10-14T12:00:00Z"; +const DAY_THREE = "2026-10-15T12:00:00Z"; +/** One day after the yanked fixture: the next daily re-sign. */ +const NEXT_DAY = { issuedAt: "2026-10-15T09:00:00Z", expiresAt: "2026-10-16T09:00:00Z" }; + +/** Signs any status payload, including ones `signReleaseStatus` refuses (e.g. an unknown state). */ async function signRaw(status: Record, key: VectorKey): Promise { const signature = await signPayload(releaseStatusSigningPayload(status as unknown as UnsignedReleaseStatus), key.privateKey); return { ...(status as unknown as UnsignedReleaseStatus), signature }; @@ -72,124 +87,204 @@ async function signRaw(status: Record, key: VectorKey): Promise async function buildVectors(): Promise { const key = await vectorKey(VECTOR_KEY_SEED_TEXT); const other = await vectorKey(OTHER_VECTOR_KEY_SEED_TEXT); - const digest = RELEASE_MANIFEST_FIXTURE.releaseDigest; + const sign = (status: Record) => signRaw(status, key); + const yankedWith = (over: Record) => sign({ ...RELEASE_STATUS_YANKED_FIXTURE, ...over }); + const publishedWith = (over: Record) => sign({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, ...over }); // The same tree released as 1.2.0 and 1.2.1: one releaseDigest, two manifest digests. const manifests: StatusVectors["manifests"] = []; for (const [name, base] of [ - ["1.2.0 of the multi-file tree", RELEASE_MANIFEST_FIXTURE], - ["1.2.1 of the same tree", { ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }], - ["minimal release", RELEASE_MANIFEST_MINIMAL_FIXTURE], + [V120, RELEASE_MANIFEST_FIXTURE], + [V121, { ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }], + [MINIMAL, RELEASE_MANIFEST_MINIMAL_FIXTURE], ] as const) { const manifest = await signReleaseManifest(base, key.privateKey); - manifests.push({ name, manifest, manifestDigest: await releaseManifestDigest(manifest) }); + manifests.push({ name, document: canonicalJson(manifest), manifestDigest: await releaseManifestDigest(manifest) }); } const nextVersionDigest = manifests[1]!.manifestDigest; - const published = await signReleaseStatus(RELEASE_STATUS_PUBLISHED_FIXTURE, key.privateKey); - const yanked = await signReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE, key.privateKey); - const nextVersionPublished = await signReleaseStatus( - { ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: nextVersionDigest }, - key.privateKey, - ); + const published = await sign({ ...RELEASE_STATUS_PUBLISHED_FIXTURE }); + const yanked = await sign({ ...RELEASE_STATUS_YANKED_FIXTURE }); + const quarantined = await yankedWith({ state: "quarantined" }); + const doc = (status: SkillxReleaseStatus) => canonicalJson(status); + const yankedDoc = doc(yanked); const valid: StatusVectors["valid"] = []; - for (const [name, status, previous, manifestDigest] of [ - ["first status of a release", published, null, RELEASE_FIXTURE_MANIFEST_DIGEST], - ["yanked with a reason after the published status", yanked, published, RELEASE_FIXTURE_MANIFEST_DIGEST], - ["identical re-fetch of the accepted status", yanked, yanked, RELEASE_FIXTURE_MANIFEST_DIGEST], - ["first status of another version of the same tree, tracked separately", nextVersionPublished, null, nextVersionDigest], - ] as const) { + const validCases: Array<[string, SkillxReleaseStatus, SkillxReleaseStatus | null, string, string]> = [ + ["first status of a release", published, null, V120, DAY_ONE], + ["yanked with a reason after the published status", yanked, published, V120, DAY_TWO], + ["identical re-fetch of the accepted status", yanked, yanked, V120, DAY_TWO], + [ + "first status of another version of the same tree, tracked separately", + await publishedWith({ manifestDigest: nextVersionDigest }), + null, + V121, + DAY_ONE, + ], + ["daily re-sign of the same state", await yankedWith({ sequence: 3, ...NEXT_DAY }), yanked, V120, DAY_THREE], + ["yanked release published again", await yankedWith({ state: "published", sequence: 3, ...NEXT_DAY }), yanked, V120, DAY_THREE], + [ + "quarantined to published across a skipped sequence (via yanked)", + await yankedWith({ state: "published", sequence: 4, ...NEXT_DAY }), + quarantined, + V120, + DAY_THREE, + ], + ["checked exactly at expiresAt", published, null, V120, RELEASE_STATUS_PUBLISHED_FIXTURE.expiresAt], + ]; + for (const [name, status, previous, manifest, now] of validCases) { const payload = releaseStatusSigningPayload(status); valid.push({ name, - expectedReleaseDigest: digest, - expectedManifestDigest: manifestDigest, - previous, - status, + manifest, + now, + previous: previous === null ? null : doc(previous), + document: doc(status), signingPayload: decoder.decode(payload), signingPayloadSha256: hex(await sha256(payload)), }); } - const stateChanged = clone(yanked); - stateChanged.state = "published"; - const reasonChanged = clone(yanked); - reasonChanged.reason = `${RELEASE_STATUS_FIXTURE_REASON}.`; - const sequenceRaised = clone(yanked); - sequenceRaised.sequence = 3; - const otherKey = await signReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE, other.privateKey); - const unknownKey = clone(yanked); - unknownKey.keyId = "skillx-unknown-key"; - const goStyle = clone(yanked); - goStyle.signature = base64( + const tamper = (mutate: (s: SkillxReleaseStatus) => void): string => { + const copy = clone(yanked); + mutate(copy); + return doc(copy); + }; + const goStyleSignature = base64( new Uint8Array( await crypto.subtle.sign({ name: "Ed25519" }, key.privateKey, goJsonStyle(releaseStatusSigningPayload(RELEASE_STATUS_YANKED_FIXTURE))), ), ); - const conflicting = await signReleaseStatus( - { ...RELEASE_STATUS_PUBLISHED_FIXTURE, sequence: 2, issuedAt: "2026-10-14T09:00:00Z" }, - key.privateKey, + const { manifestDigest: _m, ...withoutManifestDigest } = RELEASE_STATUS_PUBLISHED_FIXTURE; + const { expiresAt: _e, ...withoutExpiresAt } = RELEASE_STATUS_PUBLISHED_FIXTURE; + const mismatch = RELEASE_SIGNATURE_CODES.mismatch; + const C = RELEASE_STATUS_CODES; + + const invalid: StatusVectors["invalid"] = []; + const add = ( + name: string, + document: string, + expectedCode: string, + options: { previous?: SkillxReleaseStatus | null; manifest?: string; now?: string } = {}, + ) => + invalid.push({ + name, + manifest: options.manifest ?? V120, + now: options.now ?? DAY_TWO, + previous: options.previous ? doc(options.previous) : null, + document, + expectedCode, + }); + + // Signature. + add("state changed after signing", tamper((s) => void (s.state = "published")), mismatch, { previous: published }); + add("reason changed after signing", tamper((s) => void (s.reason = `${RELEASE_STATUS_FIXTURE_REASON}.`)), mismatch, { previous: published }); + add("sequence raised after signing", tamper((s) => void (s.sequence = 3)), mismatch, { previous: yanked }); + add("expiry extended after signing", tamper((s) => void (s.expiresAt = "2026-10-21T09:00:00Z")), mismatch); + add("signed by another key under the trusted key ID", doc(await signRaw({ ...RELEASE_STATUS_YANKED_FIXTURE }, other)), mismatch, { + previous: published, + }); + add("unknown key ID", tamper((s) => void (s.keyId = "skillx-unknown-key")), RELEASE_SIGNATURE_CODES.unknownKey, { previous: published }); + add("signature over an encoding/json-escaped payload", tamper((s) => void (s.signature = goStyleSignature)), mismatch, { previous: published }); + + // Received bytes. + add("pretty-printed", JSON.stringify(yanked, null, 2), C.nonCanonical); + add("members in insertion order", JSON.stringify(yanked), C.nonCanonical); + add("duplicate member", yankedDoc.replace("{", '{"state":"published",'), C.nonCanonical); + add("exponent number form", yankedDoc.replace('"sequence":2', '"sequence":2e0'), C.nonCanonical); + add("escaped ASCII character", yankedDoc.replace("ed25519", "ed2551\\u0039"), C.nonCanonical); + add("byte order mark", `${String.fromCodePoint(0xfeff)}${yankedDoc}`, C.nonCanonical); + add("member differing from a known one only by case, correctly signed", doc(await yankedWith({ State: "published" })), "unknown_field"); + add("empty reason injected, correctly signed", doc(await publishedWith({ reason: "" })), "length", { now: DAY_ONE }); + add("null reason injected, correctly signed", doc(await publishedWith({ reason: null })), "type", { now: DAY_ONE }); + + // Structure. + add("unknown state, correctly signed", doc(await publishedWith({ state: "revoked", sequence: 3 })), C.unknownState, { previous: yanked }); + add("missing manifestDigest, correctly signed", doc(await sign(withoutManifestDigest)), "required", { now: DAY_ONE }); + add("missing expiresAt, correctly signed", doc(await sign(withoutExpiresAt)), "required", { now: DAY_ONE }); + add("expiresAt before issuedAt, correctly signed", doc(await publishedWith({ expiresAt: "2026-10-13T04:30:59Z" })), C.invalidLifetime, { + now: DAY_ONE, + }); + add("lifetime longer than 7 days, correctly signed", doc(await publishedWith({ expiresAt: "2026-10-20T04:31:01Z" })), C.invalidLifetime, { + now: DAY_ONE, + }); + add("first status that is yanked, correctly signed", doc(await yankedWith({ sequence: 1 })), C.invalidTransition); + add( + "reason over 512 UTF-8 bytes, correctly signed", + doc(await yankedWith({ reason: String.fromCodePoint(0xe9).repeat(RELEASE_STATUS_REASON_MAX_BYTES / 2) + "." })), + "length", + ); + add( + "bidi override in the reason, correctly signed", + doc(await yankedWith({ reason: `see ${String.fromCodePoint(0x202e)}hs.kcehc` })), + "format", ); - const unknownState = await signRaw({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, state: "revoked", sequence: 3 }, key); - const otherRelease = await signReleaseStatus( - { - ...RELEASE_STATUS_PUBLISHED_FIXTURE, - releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest, - manifestDigest: RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST, - }, - key.privateKey, + + // Binding and freshness. + add( + "status of another release tree", + doc( + await publishedWith({ + releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest, + manifestDigest: RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST, + }), + ), + C.digestMismatch, + { now: DAY_ONE }, ); - const nextVersionYanked = await signReleaseStatus({ ...RELEASE_STATUS_YANKED_FIXTURE, manifestDigest: nextVersionDigest }, key.privateKey); - const { manifestDigest: _omitted, ...withoutManifestDigest } = RELEASE_STATUS_PUBLISHED_FIXTURE; - const missingManifestDigest = await signRaw(withoutManifestDigest, key); - const wrongManifestDigest = await signReleaseStatus( - { ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: `sha256:${"e".repeat(64)}` }, - key.privateKey, + add("status of another version of the same tree", doc(await yankedWith({ manifestDigest: nextVersionDigest })), C.manifestMismatch, { + previous: published, + }); + add("wrong manifestDigest, correctly signed", doc(await publishedWith({ manifestDigest: `sha256:${"e".repeat(64)}` })), C.manifestMismatch, { + now: DAY_ONE, + }); + add( + "issued before the manifest was created", + doc(await publishedWith({ issuedAt: "2026-10-13T04:29:59Z", expiresAt: "2026-10-14T04:29:59Z" })), + C.issuedBeforeRelease, + { now: DAY_ONE }, ); + add("expired one second after expiresAt", doc(published), C.expired, { now: "2026-10-14T04:31:01Z" }); - const mismatch = RELEASE_SIGNATURE_CODES.mismatch; - const invalid = (name: string, status: SkillxReleaseStatus, previous: SkillxReleaseStatus | null, expectedCode: string) => ({ - name, - expectedReleaseDigest: digest, - expectedManifestDigest: RELEASE_FIXTURE_MANIFEST_DIGEST, - previous, - status, - expectedCode, + // Against the previous accepted status. + add("lower sequence than the accepted status (rollback)", doc(published), C.rollback, { previous: yanked, now: DAY_ONE }); + add("same sequence with different content (conflict)", doc(await yankedWith({ state: "published" })), C.conflict, { previous: yanked }); + add("higher sequence issued no later (issuedAt regression)", doc(await yankedWith({ sequence: 3 })), C.issuedAtRegression, { + previous: yanked, }); + add("yanked back to draft", doc(await yankedWith({ state: "draft", sequence: 3, ...NEXT_DAY })), C.invalidTransition, { + previous: yanked, + now: DAY_THREE, + }); + add( + "quarantined straight to published", + doc(await yankedWith({ state: "published", sequence: 3, ...NEXT_DAY })), + C.invalidTransition, + { previous: quarantined, now: DAY_THREE }, + ); return { description: "skillx.release-status/v1 signed status vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", canonicalization: - "same as skillx.release/v1: padded base64 Ed25519 over the UTF-8 canonical JSON of the status without its signature member; an absent reason is omitted, never null", + "same as skillx.release/v1: padded base64 Ed25519 over the UTF-8 canonical JSON of the status without its signature member; an absent reason is omitted, never null or empty; a served document is exactly the canonical JSON including signature", manifestDigestRule: "manifestDigest = 'sha256:' + lowercase hex SHA-256 of the manifest signing payload: the UTF-8 canonical JSON of the skillx.release/v1 manifest without its signature member (the exact bytes its signature covers)", consumerRules: [ - "validate the structure; a state outside draft|quarantined|published|yanked is status.unknown_state even when signed; a missing member is required", + "verify the manifest document first (skillx.release/v1 vectors); statuses are checked against that verified manifest", + "re-encode the received document canonically; any byte difference is status.non_canonical", + "validate the structure: unknown member unknown_field; missing member required; state outside draft|quarantined|published|yanked status.unknown_state even when signed; reason 1..512 UTF-8 bytes (length, null is type) without Cc, Cf (incl. bidi controls), Zl, Zp or Cs code points (format); issuedAt < expiresAt <= issuedAt + 7 days else status.invalid_lifetime; sequence 1 must be draft or published else status.invalid_transition", "require releaseDigest to equal the verified manifest's releaseDigest, else status.digest_mismatch", "require manifestDigest to equal the verified manifest's manifest digest, else status.manifest_mismatch", + "require issuedAt >= the manifest createdAt, else status.issued_before_release", "verify the signature with the trusted key for keyId: signature.unknown_key, signature.invalid_key or signature.mismatch", - "per manifestDigest, against the last accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict; identical bytes are accepted", + "require now <= expiresAt, else status.expired; an expired status may only keep already-installed content, never authorize a new resolution or install", + "per manifestDigest, against the last accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict (identical bytes are accepted); a higher sequence needs a later issuedAt (status.issued_at_regression) and a state reachable in at most (sequence gap) steps of draft->draft|published, published->published|yanked|quarantined, yanked->yanked|published, quarantined->quarantined|yanked (status.invalid_transition)", ], testKey: describeVectorKey(RELEASE_FIXTURE_KEY_ID, VECTOR_KEY_SEED_TEXT, key), manifests, valid, - invalid: [ - invalid("state changed after signing", stateChanged, published, mismatch), - invalid("reason changed after signing", reasonChanged, published, mismatch), - invalid("sequence raised after signing", sequenceRaised, yanked, mismatch), - invalid("signed by another key under the trusted key ID", otherKey, published, mismatch), - invalid("unknown key ID", unknownKey, published, RELEASE_SIGNATURE_CODES.unknownKey), - invalid("signature over an encoding/json-escaped payload", goStyle, published, mismatch), - invalid("lower sequence than the accepted status (rollback)", published, yanked, RELEASE_STATUS_CODES.rollback), - invalid("same sequence with different content (conflict)", conflicting, yanked, RELEASE_STATUS_CODES.conflict), - invalid("unknown state, correctly signed", unknownState, yanked, RELEASE_STATUS_CODES.unknownState), - invalid("status of another release tree", otherRelease, null, RELEASE_STATUS_CODES.digestMismatch), - invalid("status of another version of the same tree", nextVersionYanked, published, RELEASE_STATUS_CODES.manifestMismatch), - invalid("missing manifestDigest, correctly signed", missingManifestDigest, null, "required"), - invalid("wrong manifestDigest, correctly signed", wrongManifestDigest, null, RELEASE_STATUS_CODES.manifestMismatch), - ], + invalid, }; } @@ -200,10 +295,12 @@ function trustedKeys(): Map { } function verifyVector(vector: StatusVector) { - return verifyReleaseStatus(vector.status, trustedKeys(), { - releaseDigest: vector.expectedReleaseDigest, - manifestDigest: vector.expectedManifestDigest, - previous: vector.previous, + const entry = committed.manifests.find((m) => m.name === vector.manifest); + if (entry === undefined) throw new Error(`unknown manifest ${vector.manifest}`); + return verifyReleaseStatus(vector.document, trustedKeys(), { + manifest: JSON.parse(entry.document) as SkillxReleaseManifest, + now: new Date(vector.now), + previous: vector.previous === null ? null : (JSON.parse(vector.previous) as SkillxReleaseStatus), }); } @@ -218,33 +315,54 @@ describe("skillx.release-status/v1 cross-language vectors", () => { it.skipIf(writeVectorsRequested)("list verified manifests with their manifest digests", async () => { expect(committed.manifests.length).toBe(3); - expect(committed.manifests[0]!.manifest.releaseDigest).toBe(committed.manifests[1]!.manifest.releaseDigest); + const [v120, v121] = committed.manifests.map((m) => JSON.parse(m.document) as SkillxReleaseManifest); + expect(v120!.releaseDigest).toBe(v121!.releaseDigest); + expect(committed.manifests[0]!.manifestDigest).toBe(RELEASE_FIXTURE_MANIFEST_DIGEST); expect(committed.manifests[0]!.manifestDigest).not.toBe(committed.manifests[1]!.manifestDigest); for (const entry of committed.manifests) { - expect(await verifyReleaseManifest(entry.manifest, trustedKeys()), entry.name).toEqual({ valid: true, issues: [] }); - expect(await releaseManifestDigest(entry.manifest), entry.name).toBe(entry.manifestDigest); + expect(await verifyReleaseManifest(entry.document, trustedKeys()), entry.name).toEqual({ valid: true, issues: [] }); + expect(await releaseManifestDigest(JSON.parse(entry.document) as SkillxReleaseManifest), entry.name).toBe(entry.manifestDigest); } }); it.skipIf(writeVectorsRequested)("accept every valid vector", async () => { - expect(committed.valid.length).toBe(4); + expect(committed.valid.length).toBe(8); for (const vector of committed.valid) { expect(await verifyVector(vector), vector.name).toEqual({ valid: true, issues: [] }); - expect(decoder.decode(releaseStatusSigningPayload(vector.status))).toBe(vector.signingPayload); + const status = JSON.parse(vector.document) as SkillxReleaseStatus; + expect(decoder.decode(releaseStatusSigningPayload(status))).toBe(vector.signingPayload); } }); - it.skipIf(writeVectorsRequested)("reject every invalid vector with its code", async () => { + it.skipIf(writeVectorsRequested)("reject every invalid vector with exactly its code", async () => { const codes = new Set(); for (const vector of committed.invalid) { expect((await verifyVector(vector)).issues.map((i) => i.code), vector.name).toEqual([vector.expectedCode]); codes.add(vector.expectedCode); } expect([...codes].sort()).toEqual( - [...Object.values(RELEASE_STATUS_CODES), RELEASE_SIGNATURE_CODES.mismatch, RELEASE_SIGNATURE_CODES.unknownKey, "required"].sort(), + [ + ...Object.values(RELEASE_STATUS_CODES), + RELEASE_SIGNATURE_CODES.mismatch, + RELEASE_SIGNATURE_CODES.unknownKey, + "required", + "unknown_field", + "length", + "type", + "format", + ].sort(), ); }); + it.skipIf(writeVectorsRequested)("bounds every status lifetime by 7 days", () => { + for (const vector of committed.valid) { + const status = JSON.parse(vector.document) as SkillxReleaseStatus; + const lifetime = (Date.parse(status.expiresAt) - Date.parse(status.issuedAt)) / 1000; + expect(lifetime).toBeGreaterThan(0); + expect(lifetime).toBeLessThanOrEqual(RELEASE_STATUS_MAX_LIFETIME_SECONDS); + } + }); + it.skipIf(writeVectorsRequested)("signs the reason with raw non-ASCII and HTML characters", () => { const yanked = committed.valid[1]!; expect(yanked.signingPayload).toContain(RELEASE_STATUS_FIXTURE_REASON); diff --git a/packages/contracts/src/release-status.test.ts b/packages/contracts/src/release-status.test.ts index 987528b..e397391 100644 --- a/packages/contracts/src/release-status.test.ts +++ b/packages/contracts/src/release-status.test.ts @@ -1,17 +1,19 @@ import { describe, expect, it } from "vitest"; +import { canonicalJson } from "./canonical-json"; +import { RELEASE_FIXTURE_KEY_ID, RELEASE_MANIFEST_FIXTURE, RELEASE_MANIFEST_MINIMAL_FIXTURE } from "./fixtures/release-fixtures"; import { - RELEASE_FIXTURE_KEY_ID, RELEASE_FIXTURE_MANIFEST_DIGEST, - RELEASE_MANIFEST_FIXTURE, - RELEASE_MANIFEST_MINIMAL_FIXTURE, RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST, RELEASE_STATUS_PUBLISHED_FIXTURE, RELEASE_STATUS_YANKED_FIXTURE, -} from "./fixtures"; +} from "./fixtures/release-status-fixtures"; import { RELEASE_SIGNATURE_CODES, ReleaseManifestError, releaseManifestDigest, signReleaseManifest } from "./release-manifest"; +import type { SkillxReleaseManifest } from "./release-manifest"; import { + RELEASE_STATE_TRANSITIONS, RELEASE_STATUS_CODES, - RELEASE_STATUS_REASON_MAX_LENGTH, + RELEASE_STATUS_REASON_MAX_BYTES, + isReachableState, releaseStatusSigningPayload, signReleaseStatus, validateReleaseStatus, @@ -22,7 +24,10 @@ import type { ReleaseStatusVerifyOptions, SkillxReleaseStatus, UnsignedReleaseSt const decoder = new TextDecoder(); const DIGEST = RELEASE_MANIFEST_FIXTURE.releaseDigest; -const BOUND: ReleaseStatusVerifyOptions = { releaseDigest: DIGEST, manifestDigest: RELEASE_FIXTURE_MANIFEST_DIGEST }; +/** Inside the lifetime of the published fixture (issued 2026-10-13T04:31:00Z). */ +const DAY_ONE = new Date("2026-10-13T12:00:00Z"); +/** Inside the lifetime of the yanked fixture (issued 2026-10-14T09:00:00Z). */ +const DAY_TWO = new Date("2026-10-14T12:00:00Z"); async function generateKey(): Promise<{ privateKey: CryptoKey; publicKey: Uint8Array }> { const pair = (await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"])) as CryptoKeyPair; @@ -33,15 +38,28 @@ function clone(value: T): T { return JSON.parse(JSON.stringify(value)) as T; } -async function signedPair() { +const doc = (value: unknown): string => canonicalJson(value); + +async function setup() { const { privateKey, publicKey } = await generateKey(); const keys = new Map([[RELEASE_FIXTURE_KEY_ID, publicKey]]); + const manifest = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); const published = await signReleaseStatus(RELEASE_STATUS_PUBLISHED_FIXTURE, privateKey); const yanked = await signReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE, privateKey); - return { privateKey, keys, published, yanked }; + const sign = (over: Partial) => signReleaseStatus({ ...RELEASE_STATUS_YANKED_FIXTURE, ...over }, privateKey); + const at = (now: Date, extra: Partial = {}): ReleaseStatusVerifyOptions => ({ manifest, now, ...extra }); + return { privateKey, keys, manifest, published, yanked, sign, at }; +} + +/** A later status of the fixture release, `days` after the yanked one. */ +function laterTimes(days: number): Pick { + const issued = Date.parse(RELEASE_STATUS_YANKED_FIXTURE.issuedAt) + days * 86_400_000; + const iso = (ms: number) => new Date(ms).toISOString().replace(".000Z", "Z"); + return { issuedAt: iso(issued), expiresAt: iso(issued + 86_400_000) }; } const codes = (result: { issues: { code: string }[] }) => result.issues.map((i) => i.code); +const OK = { valid: true, issues: [] }; describe("release manifest digest", () => { it("is the sha256 of the manifest signing payload and matches the fixture constants", async () => { @@ -50,9 +68,8 @@ describe("release manifest digest", () => { }); it("ignores the signature but distinguishes two versions of the same tree", async () => { - const { privateKey } = await generateKey(); - const signed = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, privateKey); - expect(await releaseManifestDigest(signed)).toBe(RELEASE_FIXTURE_MANIFEST_DIGEST); + const { manifest } = await setup(); + expect(await releaseManifestDigest(manifest)).toBe(RELEASE_FIXTURE_MANIFEST_DIGEST); const nextVersion = { ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }; expect(nextVersion.releaseDigest).toBe(RELEASE_MANIFEST_FIXTURE.releaseDigest); expect(await releaseManifestDigest(nextVersion)).not.toBe(RELEASE_FIXTURE_MANIFEST_DIGEST); @@ -61,12 +78,36 @@ describe("release manifest digest", () => { describe("skillx.release-status/v1 validation", () => { it("accepts both unsigned fixtures before signing", () => { - expect(validateUnsignedReleaseStatus(RELEASE_STATUS_PUBLISHED_FIXTURE)).toEqual({ valid: true, issues: [] }); - expect(validateUnsignedReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE)).toEqual({ valid: true, issues: [] }); + expect(validateUnsignedReleaseStatus(RELEASE_STATUS_PUBLISHED_FIXTURE)).toEqual(OK); + expect(validateUnsignedReleaseStatus(RELEASE_STATUS_YANKED_FIXTURE)).toEqual(OK); }); - it("rejects non-object input without throwing", () => { - for (const input of [null, undefined, "published", [], 1]) expect(validateReleaseStatus(input).valid).toBe(false); + it("rejects any non-document input without throwing", () => { + class Status { + state = "published"; + } + // eslint-disable-next-line no-sparse-arrays + const sparse = [, 1]; + const inputs: unknown[] = [ + null, + undefined, + "published", + [], + 1, + new Date(0), + new Map([["state", "published"]]), + new Status(), + Object.create(null), + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, sequence: 1n }, + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, sequence: Number.NaN }, + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, sequence: -0 }, + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, extra: sparse }, + { ...RELEASE_STATUS_PUBLISHED_FIXTURE, extra: () => 1 }, + ]; + for (const input of inputs) { + expect(() => validateReleaseStatus(input)).not.toThrow(); + expect(validateReleaseStatus(input).valid).toBe(false); + } }); it("reports every required member of an empty object, but not the optional reason", () => { @@ -74,6 +115,7 @@ describe("skillx.release-status/v1 validation", () => { const paths = result.issues.filter((i) => i.code === "required").map((i) => i.path); expect(paths.sort()).toEqual( [ + "expiresAt", "issuedAt", "keyId", "manifestDigest", @@ -93,21 +135,83 @@ describe("skillx.release-status/v1 validation", () => { expect(codes(validateUnsignedReleaseStatus(status))).toEqual(["type"]); }); - it("bounds the reason length", () => { - const at = (length: number) => ({ ...RELEASE_STATUS_YANKED_FIXTURE, reason: "x".repeat(length) }); - expect(validateUnsignedReleaseStatus(at(RELEASE_STATUS_REASON_MAX_LENGTH)).valid).toBe(true); - expect(codes(validateUnsignedReleaseStatus(at(RELEASE_STATUS_REASON_MAX_LENGTH + 1)))).toEqual(["length"]); + it("bounds the reason in UTF-8 bytes, not UTF-16 code units", () => { + const at = (reason: string) => ({ ...RELEASE_STATUS_YANKED_FIXTURE, reason }); + expect(validateUnsignedReleaseStatus(at("x".repeat(RELEASE_STATUS_REASON_MAX_BYTES))).valid).toBe(true); + expect(codes(validateUnsignedReleaseStatus(at("x".repeat(RELEASE_STATUS_REASON_MAX_BYTES + 1))))).toEqual(["length"]); + // 171 code points of 3 UTF-8 bytes each: 513 bytes in 171 UTF-16 units. + expect(codes(validateUnsignedReleaseStatus(at(String.fromCodePoint(0x20ac).repeat(171))))).toEqual(["length"]); + }); + + it.each([ + ["a tab", 0x09], + ["DEL", 0x7f], + ["a C1 control", 0x85], + ["a right-to-left override", 0x202e], + ["a left-to-right isolate", 0x2066], + ["an Arabic letter mark", 0x061c], + ["a byte order mark", 0xfeff], + ["a paragraph separator", 0x2029], + ])("rejects %s in the reason", (_name, codePoint) => { + const status = { ...RELEASE_STATUS_YANKED_FIXTURE, reason: `bad ${String.fromCodePoint(codePoint)} reason` }; + expect(codes(validateUnsignedReleaseStatus(status))).toEqual(["format"]); + }); + + it("accepts non-ASCII letters and emoji in the reason", () => { + const status = { ...RELEASE_STATUS_YANKED_FIXTURE, reason: `caf${String.fromCodePoint(0xe9)} ${String.fromCodePoint(0x1f600)}` }; + expect(validateUnsignedReleaseStatus(status)).toEqual(OK); }); it("rejects a lone surrogate in the reason", () => { const status = { ...RELEASE_STATUS_YANKED_FIXTURE, reason: `bad ${String.fromCharCode(0xd800)}` }; - expect(codes(validateUnsignedReleaseStatus(status))).toEqual(["format"]); + expect(validateUnsignedReleaseStatus(status).valid).toBe(false); }); it("rejects an unsafe sequence", () => { const status = { ...RELEASE_STATUS_PUBLISHED_FIXTURE, sequence: Number.MAX_SAFE_INTEGER + 1 }; expect(codes(validateUnsignedReleaseStatus(status))).toEqual(["type"]); }); + + it("allows only draft or published as the first status", () => { + for (const state of ["draft", "published"] as const) { + expect(validateUnsignedReleaseStatus({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, state }).valid).toBe(true); + } + for (const state of ["yanked", "quarantined"] as const) { + expect(codes(validateUnsignedReleaseStatus({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, state }))).toEqual([ + RELEASE_STATUS_CODES.invalidTransition, + ]); + } + }); + + it("bounds the lifetime to more than zero and at most 7 days", () => { + const at = (expiresAt: string) => ({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, expiresAt }); + expect(validateUnsignedReleaseStatus(at("2026-10-13T04:31:01Z")).valid).toBe(true); + expect(validateUnsignedReleaseStatus(at("2026-10-20T04:31:00Z")).valid).toBe(true); + for (const expiresAt of ["2026-10-13T04:31:00Z", "2026-10-13T04:30:59Z", "2026-10-20T04:31:01Z"]) { + expect(codes(validateUnsignedReleaseStatus(at(expiresAt)))).toEqual([RELEASE_STATUS_CODES.invalidLifetime]); + } + expect(codes(validateUnsignedReleaseStatus(at("2026-10-14")))).toEqual(["format"]); + }); +}); + +describe("release state transitions", () => { + it("matches the documented graph", () => { + expect(RELEASE_STATE_TRANSITIONS).toEqual({ + draft: ["draft", "published"], + published: ["published", "yanked", "quarantined"], + yanked: ["yanked", "published"], + quarantined: ["quarantined", "yanked"], + }); + }); + + it("never returns to draft and reaches states through skipped sequences", () => { + for (const from of ["published", "yanked", "quarantined"] as const) expect(isReachableState(from, "draft", 100)).toBe(false); + expect(isReachableState("quarantined", "published", 1)).toBe(false); + expect(isReachableState("quarantined", "published", 2)).toBe(true); + expect(isReachableState("draft", "quarantined", 1)).toBe(false); + expect(isReachableState("draft", "quarantined", 2)).toBe(true); + expect(isReachableState("published", "published", 1)).toBe(true); + }); }); describe("skillx.release-status/v1 signing payload", () => { @@ -116,17 +220,51 @@ describe("skillx.release-status/v1 signing payload", () => { expect(releaseStatusSigningPayload(reordered)).toEqual(releaseStatusSigningPayload(RELEASE_STATUS_PUBLISHED_FIXTURE)); const text = decoder.decode(releaseStatusSigningPayload(RELEASE_STATUS_PUBLISHED_FIXTURE)); expect(text).toBe( - `{"issuedAt":"2026-10-13T04:31:00Z","keyId":"skillx-test-vector-1","manifestDigest":"${RELEASE_FIXTURE_MANIFEST_DIGEST}","releaseDigest":"${DIGEST}","schemaVersion":"skillx.release-status/v1","sequence":1,"signatureAlgorithm":"ed25519","state":"published"}`, + `{"expiresAt":"2026-10-14T04:31:00Z","issuedAt":"2026-10-13T04:31:00Z","keyId":"skillx-test-vector-1","manifestDigest":"${RELEASE_FIXTURE_MANIFEST_DIGEST}","releaseDigest":"${DIGEST}","schemaVersion":"skillx.release-status/v1","sequence":1,"signatureAlgorithm":"ed25519","state":"published"}`, ); }); }); describe("skillx.release-status/v1 verification", () => { - it("verifies a first status and a later one against the accepted status", async () => { - const { keys, published, yanked } = await signedPair(); - expect(await verifyReleaseStatus(published, keys, BOUND)).toEqual({ valid: true, issues: [] }); - expect(await verifyReleaseStatus(yanked, keys, { ...BOUND, previous: published })).toEqual({ valid: true, issues: [] }); - expect(await verifyReleaseStatus(yanked, keys, { ...BOUND, previous: yanked })).toEqual({ valid: true, issues: [] }); + it("verifies a first status and a later one against the accepted status, from text or bytes", async () => { + const { keys, published, yanked, at } = await setup(); + expect(await verifyReleaseStatus(doc(published), keys, at(DAY_ONE))).toEqual(OK); + expect(await verifyReleaseStatus(new TextEncoder().encode(doc(published)), keys, at(DAY_ONE))).toEqual(OK); + expect(await verifyReleaseStatus(doc(yanked), keys, at(DAY_TWO, { previous: published }))).toEqual(OK); + expect(await verifyReleaseStatus(doc(yanked), keys, at(DAY_TWO, { previous: yanked }))).toEqual(OK); + }); + + describe("received bytes", () => { + it.each<[string, (text: string) => string | Uint8Array]>([ + ["pretty-printed", (t) => JSON.stringify(JSON.parse(t), null, 2)], + ["a trailing newline", (t) => `${t}\n`], + ["a byte order mark", (t) => `${String.fromCodePoint(0xfeff)}${t}`], + ["reordered members", (t) => JSON.stringify(Object.fromEntries(Object.entries(JSON.parse(t) as object).reverse()))], + ["a duplicate member", (t) => t.replace("{", '{"state":"published",')], + ["an exponent number form", (t) => t.replace('"sequence":2', '"sequence":2e0')], + ["an escaped ASCII character", (t) => t.replace("ed25519", "ed2551\\u0039")], + ["invalid UTF-8", (t) => new Uint8Array([...new TextEncoder().encode(t).slice(0, -1), 0xff, 0x7d])], + ["not JSON", () => "{"], + ])("rejects %s as non-canonical", async (_name, mangle) => { + const { keys, yanked, at } = await setup(); + expect(codes(await verifyReleaseStatus(mangle(doc(yanked)), keys, at(DAY_TWO)))).toEqual([RELEASE_STATUS_CODES.nonCanonical]); + }); + + it("rejects a canonical member that differs from a known one only by case", async () => { + const { keys, yanked, at } = await setup(); + const text = doc({ ...yanked, State: "published" }); + expect(codes(await verifyReleaseStatus(text, keys, at(DAY_TWO)))).toEqual(["unknown_field"]); + }); + + it.each([ + ["an empty", ""], + ["a null", null], + ])("rejects %s optional reason injected into a status without one", async (_name, reason) => { + const { keys, published, at } = await setup(); + const result = await verifyReleaseStatus(doc({ ...published, reason }), keys, at(DAY_ONE)); + expect(result.valid).toBe(false); + expect(result.issues.map((i) => i.path)).toEqual(["reason"]); + }); }); it.each<[string, (s: SkillxReleaseStatus) => void]>([ @@ -134,87 +272,146 @@ describe("skillx.release-status/v1 verification", () => { ["a changed reason", (s) => void (s.reason = "other")], ["a removed reason", (s) => void delete s.reason], ["a raised sequence", (s) => void (s.sequence = 9)], - ["a changed issue time", (s) => void (s.issuedAt = "2026-10-15T00:00:00Z")], + ["a changed issue time", (s) => void (s.issuedAt = "2026-10-14T10:00:00Z")], + ["an extended expiry", (s) => void (s.expiresAt = "2026-10-21T09:00:00Z")], ])("rejects %s", async (_name, tamper) => { - const { keys, yanked } = await signedPair(); + const { keys, yanked, at } = await setup(); const tampered = clone(yanked); tamper(tampered); - expect(codes(await verifyReleaseStatus(tampered, keys, BOUND))).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + expect(codes(await verifyReleaseStatus(doc(tampered), keys, at(DAY_TWO)))).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); }); it("rejects a manifestDigest changed after signing", async () => { - const { keys, yanked } = await signedPair(); - const other = await releaseManifestDigest({ ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }); - const tampered = { ...clone(yanked), manifestDigest: other }; - expect(codes(await verifyReleaseStatus(tampered, keys, { ...BOUND, manifestDigest: other }))).toEqual([ + const { privateKey, keys, yanked, at } = await setup(); + const otherManifest = await signReleaseManifest({ ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }, privateKey); + const tampered = { ...clone(yanked), manifestDigest: await releaseManifestDigest(otherManifest) }; + expect(codes(await verifyReleaseStatus(doc(tampered), keys, at(DAY_TWO, { manifest: otherManifest })))).toEqual([ RELEASE_SIGNATURE_CODES.mismatch, ]); }); it("rejects a status signed by another key under the trusted key ID", async () => { - const { yanked } = await signedPair(); + const { yanked, at } = await setup(); const trusted = await generateKey(); - const result = await verifyReleaseStatus(yanked, new Map([[RELEASE_FIXTURE_KEY_ID, trusted.publicKey]]), BOUND); + const result = await verifyReleaseStatus(doc(yanked), new Map([[RELEASE_FIXTURE_KEY_ID, trusted.publicKey]]), at(DAY_TWO)); expect(codes(result)).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); }); it("rejects an unknown key ID", async () => { - const { yanked, keys } = await signedPair(); - const result = await verifyReleaseStatus(yanked, new Map([["skillx-other-key", keys.get(RELEASE_FIXTURE_KEY_ID)!]]), BOUND); + const { yanked, keys, at } = await setup(); + const result = await verifyReleaseStatus(doc(yanked), new Map([["skillx-other-key", keys.get(RELEASE_FIXTURE_KEY_ID)!]]), at(DAY_TWO)); expect(codes(result)).toEqual([RELEASE_SIGNATURE_CODES.unknownKey]); }); - it("rejects a lower sequence than the accepted status", async () => { - const { keys, published, yanked } = await signedPair(); - expect(codes(await verifyReleaseStatus(published, keys, { ...BOUND, previous: yanked }))).toEqual([RELEASE_STATUS_CODES.rollback]); + describe("expiry", () => { + it("accepts a status up to and including expiresAt", async () => { + const { keys, published, at } = await setup(); + expect(await verifyReleaseStatus(doc(published), keys, at(new Date("2026-10-14T04:31:00Z")))).toEqual(OK); + }); + + it("rejects a status after expiresAt, but only once the signature verifies", async () => { + const { keys, published, at } = await setup(); + const late = at(new Date("2026-10-14T04:31:01Z")); + expect(codes(await verifyReleaseStatus(doc(published), keys, late))).toEqual([RELEASE_STATUS_CODES.expired]); + const forged = { ...clone(published), state: "draft" }; + expect(codes(await verifyReleaseStatus(doc(forged), keys, late))).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + }); }); - it("rejects a different status with the accepted sequence", async () => { - const { privateKey, keys, yanked } = await signedPair(); - const rival = await signReleaseStatus({ ...RELEASE_STATUS_YANKED_FIXTURE, state: "published" }, privateKey); - expect(codes(await verifyReleaseStatus(rival, keys, { ...BOUND, previous: yanked }))).toEqual([RELEASE_STATUS_CODES.conflict]); + it("rejects a status issued before the release manifest was created", async () => { + const { keys, sign, at } = await setup(); + const early = await sign({ state: "published", sequence: 1, issuedAt: "2026-10-13T04:29:59Z", expiresAt: "2026-10-14T04:29:59Z" }); + expect(codes(await verifyReleaseStatus(doc(early), keys, at(DAY_ONE)))).toEqual([RELEASE_STATUS_CODES.issuedBeforeRelease]); }); - it("rejects a status of another release tree", async () => { - const { keys, published } = await signedPair(); - const result = await verifyReleaseStatus(published, keys, { - releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest, - manifestDigest: RELEASE_MINIMAL_FIXTURE_MANIFEST_DIGEST, + describe("against the previous accepted status", () => { + it("rejects a lower sequence", async () => { + const { keys, published, yanked, at } = await setup(); + expect(codes(await verifyReleaseStatus(doc(published), keys, at(DAY_ONE, { previous: yanked })))).toEqual([ + RELEASE_STATUS_CODES.rollback, + ]); + }); + + it("rejects a different status with the accepted sequence", async () => { + const { keys, yanked, sign, at } = await setup(); + const rival = await sign({ state: "published" }); + expect(codes(await verifyReleaseStatus(doc(rival), keys, at(DAY_TWO, { previous: yanked })))).toEqual([ + RELEASE_STATUS_CODES.conflict, + ]); }); + + it("rejects a higher sequence that is not issued later", async () => { + const { keys, yanked, sign, at } = await setup(); + const stale = await sign({ sequence: 3 }); + expect(codes(await verifyReleaseStatus(doc(stale), keys, at(DAY_TWO, { previous: yanked })))).toEqual([ + RELEASE_STATUS_CODES.issuedAtRegression, + ]); + }); + + it("accepts a daily re-sign of the same state", async () => { + const { keys, yanked, sign, at } = await setup(); + const refreshed = await sign({ sequence: 3, ...laterTimes(1) }); + expect(await verifyReleaseStatus(doc(refreshed), keys, at(new Date("2026-10-15T12:00:00Z"), { previous: yanked }))).toEqual(OK); + }); + + it.each<[string, "draft" | "published" | "yanked" | "quarantined", "draft" | "published" | "yanked" | "quarantined", number, boolean]>([ + ["yanked to draft", "yanked", "draft", 1, false], + ["yanked to quarantined", "yanked", "quarantined", 1, false], + ["yanked to published", "yanked", "published", 1, true], + ["yanked to quarantined across a skipped sequence", "yanked", "quarantined", 2, true], + ["quarantined to published", "quarantined", "published", 1, false], + ["quarantined to published across a skipped sequence", "quarantined", "published", 2, true], + ["quarantined to draft across many skipped sequences", "quarantined", "draft", 50, false], + ])("%s", async (_name, from, to, gap, allowed) => { + const { keys, sign, at } = await setup(); + const previous = await sign({ state: from }); + const next = await sign({ state: to, sequence: 2 + gap, ...laterTimes(1) }); + const result = await verifyReleaseStatus(doc(next), keys, at(new Date("2026-10-15T12:00:00Z"), { previous })); + expect(result).toEqual(allowed ? OK : { valid: false, issues: [expect.objectContaining({ code: RELEASE_STATUS_CODES.invalidTransition })] }); + }); + }); + + it("rejects a status of another release tree", async () => { + const { privateKey, keys, published } = await setup(); + const minimal = await signReleaseManifest(RELEASE_MANIFEST_MINIMAL_FIXTURE, privateKey); + const result = await verifyReleaseStatus(doc(published), keys, { manifest: minimal, now: DAY_ONE }); expect(codes(result)).toEqual([RELEASE_STATUS_CODES.digestMismatch]); }); it("rejects a status of another version of the same tree", async () => { - const { privateKey, keys, published } = await signedPair(); - const otherVersion = await releaseManifestDigest({ ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }); + const { privateKey, keys, published, at, sign } = await setup(); + const otherManifest = await signReleaseManifest({ ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }, privateKey); + const otherVersion = await releaseManifestDigest(otherManifest); // A status for 1.2.1 does not apply to 1.2.0, although both share the tree digest. - const forOther = await signReleaseStatus({ ...RELEASE_STATUS_YANKED_FIXTURE, manifestDigest: otherVersion }, privateKey); - expect(codes(await verifyReleaseStatus(forOther, keys, BOUND))).toEqual([RELEASE_STATUS_CODES.manifestMismatch]); + const forOther = await sign({ manifestDigest: otherVersion }); + expect(codes(await verifyReleaseStatus(doc(forOther), keys, at(DAY_TWO)))).toEqual([RELEASE_STATUS_CODES.manifestMismatch]); // Sequences are tracked per manifest: 1.2.1's status at sequence 1 is not a rollback of 1.2.0's sequence 2. - const firstForOther = await signReleaseStatus({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: otherVersion }, privateKey); - expect(await verifyReleaseStatus(firstForOther, keys, { releaseDigest: DIGEST, manifestDigest: otherVersion })).toEqual({ - valid: true, - issues: [], - }); + const firstForOther = await sign({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: otherVersion }); + expect(await verifyReleaseStatus(doc(firstForOther), keys, at(DAY_ONE, { manifest: otherManifest }))).toEqual(OK); expect(published.manifestDigest).not.toBe(otherVersion); }); it("fails closed on an unknown state before checking the signature", async () => { - const { keys, published } = await signedPair(); + const { keys, published, at } = await setup(); const unknown = { ...clone(published), state: "revoked" }; - expect(codes(await verifyReleaseStatus(unknown, keys, BOUND))).toEqual([RELEASE_STATUS_CODES.unknownState]); + expect(codes(await verifyReleaseStatus(doc(unknown), keys, at(DAY_ONE)))).toEqual([RELEASE_STATUS_CODES.unknownState]); }); - it("throws when the caller passes a previous status of another manifest", async () => { - const { privateKey, keys, yanked } = await signedPair(); - const otherVersion = await releaseManifestDigest({ ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }); - const foreign = await signReleaseStatus({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: otherVersion }, privateKey); - await expect(verifyReleaseStatus(yanked, keys, { ...BOUND, previous: foreign })).rejects.toBeInstanceOf(ReleaseManifestError); + it("throws on invalid caller options", async () => { + const { privateKey, keys, yanked, at, sign, manifest } = await setup(); + const otherManifest = await signReleaseManifest({ ...RELEASE_MANIFEST_FIXTURE, version: "1.2.1" }, privateKey); + const foreign = await sign({ ...RELEASE_STATUS_PUBLISHED_FIXTURE, manifestDigest: await releaseManifestDigest(otherManifest) }); + await expect(verifyReleaseStatus(doc(yanked), keys, at(DAY_TWO, { previous: foreign }))).rejects.toBeInstanceOf(ReleaseManifestError); + await expect(verifyReleaseStatus(doc(yanked), keys, at(new Date(Number.NaN)))).rejects.toBeInstanceOf(ReleaseManifestError); + const broken = { ...manifest, fileCount: 9 } as SkillxReleaseManifest; + await expect(verifyReleaseStatus(doc(yanked), keys, at(DAY_TWO, { manifest: broken }))).rejects.toBeInstanceOf(ReleaseManifestError); }); - it("refuses to sign an unknown state", async () => { + it("refuses to sign an unknown state or an invalid lifetime", async () => { const { privateKey } = await generateKey(); - const invalid = { ...RELEASE_STATUS_PUBLISHED_FIXTURE, state: "revoked" } as unknown as UnsignedReleaseStatus; - await expect(signReleaseStatus(invalid, privateKey)).rejects.toBeInstanceOf(ReleaseManifestError); + const unknown = { ...RELEASE_STATUS_PUBLISHED_FIXTURE, state: "revoked" } as unknown as UnsignedReleaseStatus; + await expect(signReleaseStatus(unknown, privateKey)).rejects.toBeInstanceOf(ReleaseManifestError); + const forever = { ...RELEASE_STATUS_PUBLISHED_FIXTURE, expiresAt: "2027-10-13T04:31:00Z" }; + await expect(signReleaseStatus(forever, privateKey)).rejects.toBeInstanceOf(ReleaseManifestError); }); }); diff --git a/packages/contracts/src/release-status.ts b/packages/contracts/src/release-status.ts index e66dcfa..0b082a9 100644 --- a/packages/contracts/src/release-status.ts +++ b/packages/contracts/src/release-status.ts @@ -8,30 +8,47 @@ * by `manifestDigest` (`releaseManifestDigest`), because the same tree * (`releaseDigest`) may be released under several identities or versions. * + * Freshness: every status expires. `expiresAt` is after `issuedAt` and at + * most `RELEASE_STATUS_MAX_LIFETIME_SECONDS` (7 days) later; the registry + * re-signs the current status at least daily, so a consumer that fetches + * normally always holds an unexpired one. A verifier rejects a status whose + * `expiresAt` is before the caller's `now` (`status.expired`). A consumer may + * keep using an expired status only for content it already installed, never + * to authorize a new resolution or install. + * * Each new status for a release carries a strictly higher `sequence`, tracked * per `manifestDigest`, so a consumer that keeps the last status it accepted * for that manifest can reject a replayed older one: * - * - a status whose `sequence` is lower than the last accepted one is a - * rollback (`status.rollback`); - * - a status with the same `sequence` but different signed bytes is a - * conflict (`status.conflict`); an identical re-fetch is accepted; + * - a lower `sequence` than the last accepted one is a rollback + * (`status.rollback`); + * - the same `sequence` with different signed bytes is a conflict + * (`status.conflict`); an identical re-fetch is accepted; + * - a higher `sequence` must have a later `issuedAt` + * (`status.issued_at_regression`) and a state reachable by + * `RELEASE_STATE_TRANSITIONS` (`status.invalid_transition`); * - a `state` outside `RELEASE_STATES` fails closed (`status.unknown_state`), * even when correctly signed, so an older consumer never guesses. * - * Validators never throw on malformed input. + * Validators and verifiers never throw on any document input; invalid caller + * options (`manifest`, `now`, `previous`) throw `ReleaseManifestError`. */ import { canonicalJsonBytes } from "./canonical-json"; import { RELEASE_STATES, isReleaseState } from "./package-manifest"; import type { ReleaseState } from "./package-manifest"; +import { releaseManifestDigest, validateReleaseManifest } from "./release-manifest"; +import type { SkillxReleaseManifest } from "./release-manifest"; import { RELEASE_SIGNATURE_ALGORITHM, ReleaseManifestError, + checkCanonicalValue, checkFields, checkSignatureMembers, isReleaseTimestamp, + parseReceivedDocument, signPayload, + timestampMs, verifyPayload, } from "./release-signature"; import type { ReleasePublicKeys } from "./release-signature"; @@ -40,18 +57,40 @@ import type { ValidationIssue, ValidationResult } from "./validation"; import { RELEASE_DIGEST_PATTERN } from "./verification-evidence"; export const SKILLX_RELEASE_STATUS_SCHEMA_VERSION = "skillx.release-status/v1"; -/** Maximum `reason` length in UTF-16 code units. */ -export const RELEASE_STATUS_REASON_MAX_LENGTH = 512; +/** Maximum `reason` length in UTF-8 bytes. */ +export const RELEASE_STATUS_REASON_MAX_BYTES = 512; +/** Maximum `expiresAt - issuedAt`: 7 days. */ +export const RELEASE_STATUS_MAX_LIFETIME_SECONDS = 7 * 24 * 60 * 60; /** Status-specific failure codes, in addition to the signature codes. */ export const RELEASE_STATUS_CODES = { + nonCanonical: "status.non_canonical", unknownState: "status.unknown_state", + invalidLifetime: "status.invalid_lifetime", digestMismatch: "status.digest_mismatch", manifestMismatch: "status.manifest_mismatch", + issuedBeforeRelease: "status.issued_before_release", + expired: "status.expired", rollback: "status.rollback", conflict: "status.conflict", + issuedAtRegression: "status.issued_at_regression", + invalidTransition: "status.invalid_transition", } as const; +/** States a release's first status (`sequence` 1) may have. */ +export const RELEASE_INITIAL_STATES: readonly ReleaseState[] = ["draft", "published"]; + +/** + * Allowed state changes between consecutive statuses of one release. Every + * state may repeat (the daily re-sign); nothing returns to `draft`. + */ +export const RELEASE_STATE_TRANSITIONS: Readonly> = { + draft: ["draft", "published"], + published: ["published", "yanked", "quarantined"], + yanked: ["yanked", "published"], + quarantined: ["quarantined", "yanked"], +}; + export interface UnsignedReleaseStatus { schemaVersion: typeof SKILLX_RELEASE_STATUS_SCHEMA_VERSION; /** The `releaseDigest` of the `skillx.release/v1` manifest this status describes. */ @@ -61,8 +100,11 @@ export interface UnsignedReleaseStatus { state: ReleaseState; /** Optional human-readable explanation, e.g. why a release was yanked. Omit the member when there is none. */ reason?: string; + /** Not before the manifest's `createdAt`. */ issuedAt: string; - /** Strictly increasing per `releaseDigest`, starting at 1. */ + /** After `issuedAt`, at most `RELEASE_STATUS_MAX_LIFETIME_SECONDS` later. */ + expiresAt: string; + /** Strictly increasing per `manifestDigest`, starting at 1. */ sequence: number; keyId: string; signatureAlgorithm: typeof RELEASE_SIGNATURE_ALGORITHM; @@ -73,34 +115,72 @@ export interface SkillxReleaseStatus extends UnsignedReleaseStatus { } export interface ReleaseStatusVerifyOptions { - /** The `releaseDigest` of the verified release manifest the status must describe. */ - releaseDigest: string; - /** `releaseManifestDigest` of that verified manifest. */ - manifestDigest: string; + /** The verified `skillx.release/v1` manifest the status must describe. */ + manifest: SkillxReleaseManifest; + /** The consumer's current time, compared with `expiresAt`. */ + now: Date; /** - * The status this consumer last accepted for the same `manifestDigest`, if - * any. It must be a status this function accepted before; an invalid one, - * or one for another manifest, throws. + * The status this consumer last accepted for the same manifest, if any. It + * must be a status this function accepted before; an invalid one, or one + * for another manifest, throws. */ previous?: SkillxReleaseStatus | null; } -const STATUS_FIELDS = ["schemaVersion", "releaseDigest", "manifestDigest", "state", "issuedAt", "sequence", "keyId", "signatureAlgorithm"] as const; +const STATUS_FIELDS = [ + "schemaVersion", + "releaseDigest", + "manifestDigest", + "state", + "issuedAt", + "expiresAt", + "sequence", + "keyId", + "signatureAlgorithm", +] as const; const OPTIONAL_STATUS_FIELDS = ["reason"] as const; +const TIMESTAMP_FORMAT = "must be an RFC 3339 UTC timestamp with whole seconds, e.g. 2026-10-13T04:30:00Z"; +/** Controls, format characters (including every bidi control), line and paragraph separators, surrogates. */ +const FORBIDDEN_REASON_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cs}]/u; +/** Longest chain of distinct transitions; a larger sequence gap allows nothing more. */ +const MAX_TRANSITION_STEPS = 4; function validateReason(reason: unknown, issues: ValidationIssue[]): void { if (typeof reason !== "string") { issues.push(issue("reason", "type", "must be a string when present")); - } else if (reason.length === 0 || reason.length > RELEASE_STATUS_REASON_MAX_LENGTH) { - issues.push(issue("reason", "length", `must be 1 to ${RELEASE_STATUS_REASON_MAX_LENGTH} characters`)); - } else if (/\p{Cc}|\p{Cs}/u.test(reason)) { - issues.push(issue("reason", "format", "must not contain control characters or lone surrogates")); + return; + } + const bytes = new TextEncoder().encode(reason).length; + if (bytes === 0 || bytes > RELEASE_STATUS_REASON_MAX_BYTES) { + issues.push(issue("reason", "length", `must be 1 to ${RELEASE_STATUS_REASON_MAX_BYTES} UTF-8 bytes`)); + } else if (FORBIDDEN_REASON_CHARACTERS.test(reason)) { + issues.push(issue("reason", "format", "must not contain control, format, bidi or line separator characters")); + } +} + +function validateLifetime(input: Record, issues: ValidationIssue[]): void { + const issuedOk = isReleaseTimestamp(input.issuedAt); + const expiresOk = isReleaseTimestamp(input.expiresAt); + // A missing member is already reported as `required`. + if (!issuedOk && "issuedAt" in input) issues.push(issue("issuedAt", "format", TIMESTAMP_FORMAT)); + if (!expiresOk && "expiresAt" in input) issues.push(issue("expiresAt", "format", TIMESTAMP_FORMAT)); + if (!issuedOk || !expiresOk) return; + const lifetime = (timestampMs(input.expiresAt as string) - timestampMs(input.issuedAt as string)) / 1000; + if (lifetime <= 0 || lifetime > RELEASE_STATUS_MAX_LIFETIME_SECONDS) { + issues.push( + issue( + "expiresAt", + RELEASE_STATUS_CODES.invalidLifetime, + `must be after issuedAt and at most ${RELEASE_STATUS_MAX_LIFETIME_SECONDS} seconds later`, + ), + ); } } function collectIssues(input: unknown, requireSignature: boolean): ValidationIssue[] { if (!isPlainObject(input)) return [issue("", "type", "release status must be an object")]; const issues: ValidationIssue[] = []; + if (!checkCanonicalValue(input, issues)) return issues; const required = requireSignature ? [...STATUS_FIELDS, "signature"] : STATUS_FIELDS; checkFields(input, required, "", SKILLX_RELEASE_STATUS_SCHEMA_VERSION, issues, OPTIONAL_STATUS_FIELDS); @@ -113,15 +193,19 @@ function collectIssues(input: unknown, requireSignature: boolean): ValidationIss issues.push(issue(field, "pattern", "must match sha256:<64 lowercase hex>")); } } - if ("state" in input && !isReleaseState(input.state)) { + const stateOk = isReleaseState(input.state); + if ("state" in input && !stateOk) { issues.push(issue("state", RELEASE_STATUS_CODES.unknownState, `must be one of: ${RELEASE_STATES.join(", ")}`)); } if ("reason" in input) validateReason(input.reason, issues); - if (!isReleaseTimestamp(input.issuedAt)) { - issues.push(issue("issuedAt", "format", "must be an RFC 3339 UTC timestamp with whole seconds, e.g. 2026-10-13T04:30:00Z")); - } - if (!Number.isSafeInteger(input.sequence) || (input.sequence as number) < 1) { + validateLifetime(input, issues); + const sequencePresent = "sequence" in input; + if (sequencePresent && (!Number.isSafeInteger(input.sequence) || (input.sequence as number) < 1)) { issues.push(issue("sequence", "type", "must be a positive safe integer")); + } else if (input.sequence === 1 && stateOk && !RELEASE_INITIAL_STATES.includes(input.state as ReleaseState)) { + issues.push( + issue("state", RELEASE_STATUS_CODES.invalidTransition, `the first status must be one of: ${RELEASE_INITIAL_STATES.join(", ")}`), + ); } checkSignatureMembers(input, requireSignature, issues); return issues; @@ -152,53 +236,101 @@ export async function signReleaseStatus(status: UnsignedReleaseStatus, privateKe return { ...status, signature: await signPayload(releaseStatusSigningPayload(status), privateKey) }; } +/** Whether `to` is reachable from `from` in at most `steps` transitions. */ +export function isReachableState(from: ReleaseState, to: ReleaseState, steps: number): boolean { + let frontier = new Set([from]); + for (let i = 0; i < Math.min(steps, MAX_TRANSITION_STEPS); i++) { + const next = new Set(); + for (const state of frontier) for (const target of RELEASE_STATE_TRANSITIONS[state]) next.add(target); + if (next.has(to)) return true; + frontier = next; + } + return false; +} + function sameBytes(a: Uint8Array, b: Uint8Array): boolean { return a.length === b.length && a.every((byte, i) => byte === b[i]); } +function fail(path: string, code: string, message: string): ValidationResult { + return validationFail([issue(path, code, message)]); +} + +function checkPrevious( + status: SkillxReleaseStatus, + payload: Uint8Array, + previous: SkillxReleaseStatus, + manifestDigest: string, +): ValidationResult { + const previousResult = validateReleaseStatus(previous); + if (!previousResult.valid || previous.manifestDigest !== manifestDigest) { + throw new ReleaseManifestError("previous status is not an accepted status of this release manifest", previousResult.issues); + } + if (status.sequence < previous.sequence) { + return fail("sequence", RELEASE_STATUS_CODES.rollback, `is lower than the last accepted sequence ${previous.sequence}`); + } + if (status.sequence === previous.sequence) { + return sameBytes(payload, releaseStatusSigningPayload(previous)) + ? validationOk() + : fail("sequence", RELEASE_STATUS_CODES.conflict, `differs from the accepted status with the same sequence ${previous.sequence}`); + } + if (timestampMs(status.issuedAt) <= timestampMs(previous.issuedAt)) { + return fail("issuedAt", RELEASE_STATUS_CODES.issuedAtRegression, `must be after the last accepted issuedAt ${previous.issuedAt}`); + } + if (!isReachableState(previous.state, status.state, status.sequence - previous.sequence)) { + return fail( + "state", + RELEASE_STATUS_CODES.invalidTransition, + `cannot follow ${previous.state} (sequence ${previous.sequence}) at sequence ${status.sequence}`, + ); + } + return validationOk(); +} + /** - * Validates a signed status, binds it to `options.releaseDigest` and - * `options.manifestDigest`, verifies its - * signature against `publicKeys` and, when `options.previous` is given, - * enforces the monotonic sequence. Fails closed on every malformed input, an - * unknown state, an unknown or malformed key, a bad signature, a rollback or - * a conflict; it never throws for `input`. + * Verifies a received status document for `options.manifest`, in this order: + * canonical bytes (`status.non_canonical`), structure (including + * `status.unknown_state`, `status.invalid_lifetime` and the first-status + * rule), `status.digest_mismatch`, `status.manifest_mismatch`, + * `status.issued_before_release`, the signature, `status.expired` against + * `options.now`, then, with `options.previous`, `status.rollback`, + * `status.conflict`, `status.issued_at_regression` and + * `status.invalid_transition`. Never throws for `document`; invalid options + * throw `ReleaseManifestError`. */ export async function verifyReleaseStatus( - input: unknown, + document: string | Uint8Array, publicKeys: ReleasePublicKeys, options: ReleaseStatusVerifyOptions, ): Promise { - const structure = validateReleaseStatus(input); + const manifestResult = validateReleaseManifest(options.manifest); + if (!manifestResult.valid) throw new ReleaseManifestError("manifest option is not a valid release manifest", manifestResult.issues); + const now = options.now instanceof Date ? options.now.getTime() : Number.NaN; + if (!Number.isFinite(now)) throw new ReleaseManifestError("now option must be a valid Date"); + const manifestDigest = await releaseManifestDigest(options.manifest); + + const parsed = parseReceivedDocument(document, RELEASE_STATUS_CODES.nonCanonical); + if (!("value" in parsed)) return parsed; + const structure = validateReleaseStatus(parsed.value); if (!structure.valid) return structure; - const status = input as SkillxReleaseStatus; - if (status.releaseDigest !== options.releaseDigest) { - return validationFail([issue("releaseDigest", RELEASE_STATUS_CODES.digestMismatch, "does not describe the expected release tree")]); + const status = parsed.value as SkillxReleaseStatus; + if (status.releaseDigest !== options.manifest.releaseDigest) { + return fail("releaseDigest", RELEASE_STATUS_CODES.digestMismatch, "does not describe the expected release tree"); } - if (status.manifestDigest !== options.manifestDigest) { - return validationFail([ - issue("manifestDigest", RELEASE_STATUS_CODES.manifestMismatch, "describes another release manifest of the same tree"), - ]); + if (status.manifestDigest !== manifestDigest) { + return fail("manifestDigest", RELEASE_STATUS_CODES.manifestMismatch, "describes another release manifest"); + } + if (timestampMs(status.issuedAt) < timestampMs(options.manifest.createdAt)) { + return fail("issuedAt", RELEASE_STATUS_CODES.issuedBeforeRelease, `is before the manifest createdAt ${options.manifest.createdAt}`); } const payload = releaseStatusSigningPayload(status); const failure = await verifyPayload(payload, status.signature, status.keyId, publicKeys); if (failure !== null) return validationFail([failure]); + if (now > timestampMs(status.expiresAt)) { + return fail("expiresAt", RELEASE_STATUS_CODES.expired, `expired at ${status.expiresAt}`); + } const previous = options.previous; if (previous === undefined || previous === null) return validationOk(); - const previousResult = validateReleaseStatus(previous); - if (!previousResult.valid || previous.releaseDigest !== options.releaseDigest || previous.manifestDigest !== options.manifestDigest) { - throw new ReleaseManifestError("previous status is not an accepted status of this release manifest", previousResult.issues); - } - if (status.sequence < previous.sequence) { - return validationFail([ - issue("sequence", RELEASE_STATUS_CODES.rollback, `is lower than the last accepted sequence ${previous.sequence}`), - ]); - } - if (status.sequence === previous.sequence && !sameBytes(payload, releaseStatusSigningPayload(previous))) { - return validationFail([ - issue("sequence", RELEASE_STATUS_CODES.conflict, `differs from the accepted status with the same sequence ${previous.sequence}`), - ]); - } - return validationOk(); + return checkPrevious(status, payload, previous, manifestDigest); } diff --git a/packages/contracts/src/release-vectors.test.ts b/packages/contracts/src/release-vectors.test.ts index 9e57e2c..6a89fb0 100644 --- a/packages/contracts/src/release-vectors.test.ts +++ b/packages/contracts/src/release-vectors.test.ts @@ -1,20 +1,26 @@ import { describe, expect, it } from "vitest"; +import { canonicalJson } from "./canonical-json"; import { RELEASE_FIXTURE_CONTENTS, RELEASE_FIXTURE_KEY_ID, RELEASE_MANIFEST_FIXTURE, RELEASE_MANIFEST_FIXTURES, RELEASE_MANIFEST_MINIMAL_FIXTURE, -} from "./fixtures"; +} from "./fixtures/release-fixtures"; import vectorFile from "./fixtures/release-manifest-v1.json"; import { + RELEASE_MANIFEST_CODES, RELEASE_SIGNATURE_CODES, + compareUtf8, releaseSigningPayload, + releaseTreeDigest, signReleaseManifest, validateReleaseManifest, verifyReleaseManifest, } from "./release-manifest"; -import type { SkillxReleaseManifest } from "./release-manifest"; +import type { ReleaseFile, SkillxReleaseManifest, UnsignedReleaseManifest } from "./release-manifest"; +import { signPayload } from "./release-signature"; +import { serializeTree } from "@skillx/skill-tree/rules"; import { OTHER_VECTOR_KEY_SEED_TEXT, VECTOR_KEY_SEED_TEXT, @@ -31,13 +37,14 @@ import { writeVectorFile, writeVectorsRequested, } from "./test-support/vector-key"; -import type { VectorKeyDescription } from "./test-support/vector-key"; +import type { VectorKey, VectorKeyDescription } from "./test-support/vector-key"; /** * Golden cross-language vectors for `skillx.release/v1`, exported for * AgentKit (`fixtures/release-manifest-v1.json`). The signing key is derived * from a public seed text, so the vectors are reproducible and the key is - * worthless outside tests. + * worthless outside tests. A verifier receives `document` (the exact served + * text) and must reach the vector's result from those bytes alone. * * Regenerate after an intentional contract change with * `SKILLX_WRITE_RELEASE_VECTORS=1 pnpm vitest run packages/contracts/src`. @@ -46,20 +53,61 @@ import type { VectorKeyDescription } from "./test-support/vector-key"; interface ReleaseVectors { description: string; canonicalization: string; + verifierRules: string[]; testKey: VectorKeyDescription; valid: Array<{ name: string; + document: string; manifest: SkillxReleaseManifest; signingPayload: string; signingPayloadSha256: string; + treeSerialization: string; fileContentsBase64: Record; }>; - invalidSignature: Array<{ name: string; manifest: SkillxReleaseManifest; expectedCode: string }>; + rejected: Array<{ name: string; document: string; expectedCode: string }>; invalidManifest: Array<{ name: string; manifest: unknown; expectedCode: string }>; } -function contentsBase64(name: keyof typeof RELEASE_FIXTURE_CONTENTS): Record { - return Object.fromEntries(Object.entries(RELEASE_FIXTURE_CONTENTS[name]).map(([path, text]) => [path, base64(encoder.encode(text))])); +/** + * Paths whose UTF-8 byte order (U+FF61 before U+1F600) is the reverse of + * their UTF-16 code unit order, where the surrogate pair 0xD83D sorts first. + */ +const ORDERING_CONTENTS: Readonly> = { + "SKILL.md": "---\nname: path-order\ndescription: Path ordering vector.\n---\n", + [`references/${String.fromCodePoint(0xff61)}.md`]: "Halfwidth ideographic full stop.\n", + [`references/${String.fromCodePoint(0x1f600)}.md`]: "Astral-plane emoji.\n", +}; + +async function treeFiles(contents: Readonly>): Promise { + const files: ReleaseFile[] = []; + for (const [path, text] of Object.entries(contents)) { + const bytes = encoder.encode(text); + files.push({ path, mode: "100644", sha256: hex(await sha256(bytes)), size: bytes.length }); + } + return files.sort((a, b) => compareUtf8(a.path, b.path)); +} + +async function orderingManifest(): Promise { + const files = await treeFiles(ORDERING_CONTENTS); + return { + ...RELEASE_MANIFEST_MINIMAL_FIXTURE, + identity: "@solo.dev/path-order", + version: "0.2.0", + releaseDigest: await releaseTreeDigest(files), + files, + fileCount: files.length, + treeSize: files.reduce((sum, f) => sum + f.size, 0), + }; +} + +function contentsBase64(contents: Readonly>): Record { + return Object.fromEntries(Object.entries(contents).map(([path, text]) => [path, base64(encoder.encode(text))])); +} + +/** Signs any manifest payload, including one `signReleaseManifest` refuses. */ +async function signRaw(manifest: Record, key: VectorKey): Promise { + const signature = await signPayload(releaseSigningPayload(manifest as unknown as UnsignedReleaseManifest), key.privateKey); + return { ...(manifest as unknown as UnsignedReleaseManifest), signature }; } async function buildVectors(): Promise { @@ -68,51 +116,102 @@ async function buildVectors(): Promise { const valid: ReleaseVectors["valid"] = []; const signedFull = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, key.privateKey); - for (const [name, base, tree] of [ - ["publisher-verified multi-file release", RELEASE_MANIFEST_FIXTURE, "full"], - ["unverified minimal prerelease with unknown provenance", RELEASE_MANIFEST_MINIMAL_FIXTURE, "minimal"], + for (const [name, base, contents] of [ + ["publisher-verified multi-file release", RELEASE_MANIFEST_FIXTURE, RELEASE_FIXTURE_CONTENTS.full], + ["unverified minimal prerelease with unknown provenance", RELEASE_MANIFEST_MINIMAL_FIXTURE, RELEASE_FIXTURE_CONTENTS.minimal], + ["paths ordered by UTF-8 bytes, not UTF-16 code units", await orderingManifest(), ORDERING_CONTENTS], ] as const) { const manifest = await signReleaseManifest(base, key.privateKey); const payload = releaseSigningPayload(manifest); valid.push({ name, + document: canonicalJson(manifest), manifest, signingPayload: decoder.decode(payload), signingPayloadSha256: hex(await sha256(payload)), - fileContentsBase64: contentsBase64(tree), + treeSerialization: serializeTree(manifest.files), + fileContentsBase64: contentsBase64(contents), }); } + const full = canonicalJson(signedFull); const mismatch = RELEASE_SIGNATURE_CODES.mismatch; - const versionChanged = clone(signedFull); - versionChanged.version = "1.2.1"; - const flipped = clone(signedFull); - const signatureBytes = fromBase64(flipped.signature); - signatureBytes[0] = signatureBytes[0]! ^ 0x01; - flipped.signature = base64(signatureBytes); - const otherKey = await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, other.privateKey); - const unknownKey = clone(signedFull); - unknownKey.keyId = "skillx-unknown-key"; - const goStyle = clone(signedFull); - goStyle.signature = base64( - new Uint8Array( - await crypto.subtle.sign({ name: "Ed25519" }, key.privateKey, goJsonStyle(releaseSigningPayload(RELEASE_MANIFEST_FIXTURE))), - ), + const tamper = (mutate: (m: SkillxReleaseManifest) => void): string => { + const copy = clone(signedFull); + mutate(copy); + return canonicalJson(copy); + }; + const flipped = tamper((m) => { + const bytes = fromBase64(m.signature); + bytes[0] = bytes[0]! ^ 0x01; + m.signature = base64(bytes); + }); + const goStyleSignature = base64( + new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key.privateKey, goJsonStyle(releaseSigningPayload(RELEASE_MANIFEST_FIXTURE)))), ); + const wrongTree = await signRaw({ ...RELEASE_MANIFEST_FIXTURE, releaseDigest: RELEASE_MANIFEST_MINIMAL_FIXTURE.releaseDigest }, key); + const fileHashChanged = clone(RELEASE_MANIFEST_FIXTURE); + fileHashChanged.files[0]!.sha256 = `${fileHashChanged.files[0]!.sha256.slice(0, 63)}0`; + const signedFileHashChanged = await signRaw(fileHashChanged as unknown as Record, key); + const nonCanonical = RELEASE_MANIFEST_CODES.nonCanonical; return { description: "skillx.release/v1 signed manifest vectors. The test key is derived from a public seed text and MUST NOT be trusted outside tests.", canonicalization: "signature = padded base64 Ed25519 over UTF-8 canonical JSON (RFC 8785 subset: members sorted by UTF-16 code units, no whitespace, non-negative safe integers only, only quote/backslash/C0 controls escaped, everything else raw UTF-8) of the manifest without its signature member; see packages/contracts/src/canonical-json.ts", + verifierRules: [ + "parse document and re-encode it canonically; any byte difference (whitespace, member order, duplicate member, escapes, number form, BOM, trailing bytes, invalid UTF-8) is release.non_canonical", + "validate the structure; an unknown member, including one differing from a known member only by case, is unknown_field", + "require releaseDigest == 'sha256:' + hex SHA-256 of treeSerialization (agentkit.skill-tree/v1 of files), else release.tree_digest_mismatch", + "verify the signature with the trusted key for keyId over the canonical JSON without signature: signature.unknown_key, signature.invalid_key or signature.mismatch", + ], testKey: describeVectorKey(RELEASE_FIXTURE_KEY_ID, VECTOR_KEY_SEED_TEXT, key), valid, - invalidSignature: [ - { name: "version changed after signing", manifest: versionChanged, expectedCode: mismatch }, - { name: "flipped signature byte", manifest: flipped, expectedCode: mismatch }, - { name: "signed by another key under the trusted key ID", manifest: otherKey, expectedCode: mismatch }, - { name: "unknown key ID", manifest: unknownKey, expectedCode: RELEASE_SIGNATURE_CODES.unknownKey }, - { name: "signature over an encoding/json-escaped payload", manifest: goStyle, expectedCode: mismatch }, + rejected: [ + { name: "version changed after signing", document: tamper((m) => void (m.version = "1.2.1")), expectedCode: mismatch }, + { name: "flipped signature byte", document: flipped, expectedCode: mismatch }, + { + name: "signed by another key under the trusted key ID", + document: canonicalJson(await signReleaseManifest(RELEASE_MANIFEST_FIXTURE, other.privateKey)), + expectedCode: mismatch, + }, + { + name: "unknown key ID", + document: tamper((m) => void (m.keyId = "skillx-unknown-key")), + expectedCode: RELEASE_SIGNATURE_CODES.unknownKey, + }, + { + name: "signature over an encoding/json-escaped payload", + document: tamper((m) => void (m.signature = goStyleSignature)), + expectedCode: mismatch, + }, + { + name: "releaseDigest is not the tree digest of files, correctly signed", + document: canonicalJson(wrongTree), + expectedCode: RELEASE_MANIFEST_CODES.treeDigestMismatch, + }, + { + name: "file hash changed, correctly signed", + document: canonicalJson(signedFileHashChanged), + expectedCode: RELEASE_MANIFEST_CODES.treeDigestMismatch, + }, + { + name: "member differing from a known one only by case", + document: canonicalJson({ ...signedFull, Version: "1.2.0" }), + expectedCode: "unknown_field", + }, + { name: "pretty-printed", document: JSON.stringify(signedFull, null, 2), expectedCode: nonCanonical }, + { + name: "members in insertion order", + document: JSON.stringify(signedFull), + expectedCode: nonCanonical, + }, + { name: "duplicate member", document: full.replace("{", '{"version":"9.9.9",'), expectedCode: nonCanonical }, + { name: "exponent number form", document: full.replace('"artifactSize":10240', '"artifactSize":1.024e4'), expectedCode: nonCanonical }, + { name: "escaped solidus", document: full.replace("scripts/check.sh", "scripts\\/check.sh"), expectedCode: nonCanonical }, + { name: "byte order mark", document: `${String.fromCodePoint(0xfeff)}${full}`, expectedCode: nonCanonical }, + { name: "trailing newline", document: `${full}\n`, expectedCode: nonCanonical }, ], invalidManifest: RELEASE_MANIFEST_FIXTURES.filter((c) => !c.expectedValid).map((c) => ({ name: c.name, @@ -124,6 +223,10 @@ async function buildVectors(): Promise { const committed = vectorFile as unknown as ReleaseVectors; +function trustedKeys(): Map { + return new Map([[committed.testKey.keyId, fromBase64(committed.testKey.publicKeyBase64)]]); +} + describe("skillx.release/v1 cross-language vectors", () => { it.runIf(writeVectorsRequested)("writes the vector file", async () => { await writeVectorFile("release-manifest-v1.json", await buildVectors()); @@ -133,34 +236,45 @@ describe("skillx.release/v1 cross-language vectors", () => { expect(committed).toEqual(await buildVectors()); }); - it.skipIf(writeVectorsRequested)("verify every valid vector with the published test key", async () => { - const keys = new Map([[committed.testKey.keyId, fromBase64(committed.testKey.publicKeyBase64)]]); - expect(committed.valid.length).toBe(2); + it.skipIf(writeVectorsRequested)("verify every valid vector from its document with the published test key", async () => { + expect(committed.valid.length).toBe(3); for (const vector of committed.valid) { - expect(await verifyReleaseManifest(vector.manifest, keys), vector.name).toEqual({ valid: true, issues: [] }); + expect(await verifyReleaseManifest(vector.document, trustedKeys()), vector.name).toEqual({ valid: true, issues: [] }); + expect(vector.document).toBe(canonicalJson(vector.manifest)); expect(decoder.decode(releaseSigningPayload(vector.manifest))).toBe(vector.signingPayload); - expect(Object.keys(vector.fileContentsBase64)).toEqual(vector.manifest.files.map((f) => f.path)); + expect(serializeTree(vector.manifest.files)).toBe(vector.treeSerialization); + expect(Object.keys(vector.fileContentsBase64).sort(compareUtf8)).toEqual(vector.manifest.files.map((f) => f.path)); expect(vector.manifest).not.toHaveProperty("state"); } }); - it.skipIf(writeVectorsRequested)("hash every vector file to its manifest entry", async () => { + it.skipIf(writeVectorsRequested)("order the ordering vector's paths differently from UTF-16 code units", () => { + const paths = committed.valid[2]!.manifest.files.map((f) => f.path); + expect([...paths].sort()).not.toEqual(paths); + expect([...paths].sort(compareUtf8)).toEqual(paths); + }); + + it.skipIf(writeVectorsRequested)("hash every vector file to its manifest entry and the tree to releaseDigest", async () => { for (const vector of committed.valid) { for (const file of vector.manifest.files) { const bytes = fromBase64(vector.fileContentsBase64[file.path]!); expect(bytes.length, file.path).toBe(file.size); expect(hex(await sha256(bytes)), file.path).toBe(file.sha256); } + expect(`sha256:${hex(await sha256(encoder.encode(vector.treeSerialization)))}`).toBe(vector.manifest.releaseDigest); } }); - it.skipIf(writeVectorsRequested)("reject every invalid-signature vector with its code", async () => { - const keys = new Map([[committed.testKey.keyId, fromBase64(committed.testKey.publicKeyBase64)]]); - for (const vector of committed.invalidSignature) { - expect(validateReleaseManifest(vector.manifest).valid, vector.name).toBe(true); - const result = await verifyReleaseManifest(vector.manifest, keys); + it.skipIf(writeVectorsRequested)("reject every rejected document with exactly its code", async () => { + const codes = new Set(); + for (const vector of committed.rejected) { + const result = await verifyReleaseManifest(vector.document, trustedKeys()); expect(result.issues.map((i) => i.code), vector.name).toEqual([vector.expectedCode]); + codes.add(vector.expectedCode); } + expect([...codes].sort()).toEqual( + [...Object.values(RELEASE_MANIFEST_CODES), RELEASE_SIGNATURE_CODES.mismatch, RELEASE_SIGNATURE_CODES.unknownKey, "unknown_field"].sort(), + ); }); it.skipIf(writeVectorsRequested)("reject every invalid-manifest vector with its code", () => { diff --git a/packages/skill-tree/test/release-manifest-vectors.test.ts b/packages/skill-tree/test/release-manifest-vectors.test.ts index 7845b70..ee0b88e 100644 --- a/packages/skill-tree/test/release-manifest-vectors.test.ts +++ b/packages/skill-tree/test/release-manifest-vectors.test.ts @@ -3,7 +3,7 @@ import * as root from "../src/index"; import * as rules from "../src/rules"; import { LIMITS, treeDigest } from "../src/index"; import type { TreeFile } from "../src/index"; -import { sha256Hex } from "../src/serialize"; +import { sha256Hex, sortFiles } from "../src/serialize"; // The release contract lives in @skillx/contracts, which depends on this package, so the // guard that both packages agree on the tree imports it by path instead of by name. import { RELEASE_TREE_LIMITS } from "../../contracts/src/release-manifest"; @@ -12,6 +12,7 @@ import vectorFile from "../../contracts/src/fixtures/release-manifest-v1.json"; interface ReleaseVector { name: string; manifest: { releaseDigest: string; files: TreeFile[]; fileCount: number; treeSize: number }; + treeSerialization: string; fileContentsBase64: Record; } @@ -27,8 +28,9 @@ describe("skillx.release/v1 vectors against agentkit.skill-tree/v1", () => { const bytes = Buffer.from(vector.fileContentsBase64[f.path] ?? "", "base64"); return { path: f.path, mode: f.mode, size: bytes.length, sha256: sha256Hex(bytes) }; }); - expect(files).toEqual(vector.manifest.files); + expect(sortFiles(files)).toEqual(vector.manifest.files); expect(treeDigest(files)).toBe(vector.manifest.releaseDigest); + expect(rules.serializeTree(files)).toBe(vector.treeSerialization); expect(vector.manifest.fileCount).toBe(files.length); expect(vector.manifest.treeSize).toBe(files.reduce((sum, f) => sum + f.size, 0)); }); @@ -42,5 +44,15 @@ describe("skillx.release/v1 vectors against agentkit.skill-tree/v1", () => { expect(rules.checkPublisher).toBe(root.checkPublisher); expect(rules.foldKey).toBe(root.foldKey); expect(rules.SkillTreeError).toBe(root.SkillTreeError); + expect(rules.serializeTree).toBe(root.serializeTree); + expect(rules.compareUtf8).toBe(root.compareUtf8); + expect(rules.sortFiles).toBe(sortFiles); + expect(rules.TREE_SCHEMA).toBe(root.TREE_SCHEMA); + }); + + it("digests exactly the bytes the pure serializer renders", () => { + for (const vector of vectors) { + expect(`sha256:${sha256Hex(rules.serializeTree(vector.manifest.files))}`).toBe(vector.manifest.releaseDigest); + } }); }); From 35d2ba83a03814ae7b9acd750ebded795edd746a Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 12:31:11 +0700 Subject: [PATCH 13/14] fix(web): make the release tree digest rollback re-appliable Record the d1_migrations row removal in the 0011 rollback SQL so a later apply runs the migration again, and select migrations by name in its test so a future migration does not shift the 0010 baseline. --- .../migrations/0011_release_tree_digest.sql | 5 +++- .../db/release-tree-digest-migration.test.ts | 30 +++++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apps/web/drizzle/migrations/0011_release_tree_digest.sql b/apps/web/drizzle/migrations/0011_release_tree_digest.sql index 8d069ba..ca7d11d 100644 --- a/apps/web/drizzle/migrations/0011_release_tree_digest.sql +++ b/apps/web/drizzle/migrations/0011_release_tree_digest.sql @@ -13,7 +13,9 @@ -- created them. Those statements were removed here; applying them would fail with "table -- already exists". The 0011 snapshot records the full schema, which realigns the generator. -- --- Rollback (SQLite/D1 support DROP COLUMN since 3.35; take a D1 time-travel bookmark first): +-- Rollback (SQLite/D1 support DROP COLUMN since 3.35; take a D1 time-travel bookmark first). +-- The last statement removes the wrangler bookkeeping row, so a later `migrations apply` +-- re-applies 0011 instead of skipping it: -- DROP INDEX IF EXISTS idx_package_releases_digest; -- ALTER TABLE package_releases DROP COLUMN status_issued_at; -- ALTER TABLE package_releases DROP COLUMN status_sequence; @@ -24,6 +26,7 @@ -- ALTER TABLE package_releases DROP COLUMN release_digest; -- ALTER TABLE packages DROP COLUMN visibility; -- ALTER TABLE publishers DROP COLUMN state; +-- DELETE FROM d1_migrations WHERE name = '0011_release_tree_digest.sql'; ALTER TABLE `publishers` ADD `state` text DEFAULT 'active' NOT NULL;--> statement-breakpoint ALTER TABLE `packages` ADD `visibility` text DEFAULT 'public' NOT NULL;--> statement-breakpoint diff --git a/apps/web/tests/db/release-tree-digest-migration.test.ts b/apps/web/tests/db/release-tree-digest-migration.test.ts index 7e547a2..b4fb954 100644 --- a/apps/web/tests/db/release-tree-digest-migration.test.ts +++ b/apps/web/tests/db/release-tree-digest-migration.test.ts @@ -15,6 +15,10 @@ const MIGRATION_0011 = "0011_release_tree_digest.sql"; const migrationFiles = readdirSync(MIGRATIONS_DIR) .filter((file) => file.endsWith(".sql")) .sort(); +/** Every migration up to and including 0011, selected by name so later migrations do not shift it. */ +const THROUGH_0011 = migrationFiles.filter((file) => file <= MIGRATION_0011); +/** The database shape 0011 applies to. */ +const BEFORE_0011 = migrationFiles.filter((file) => file < MIGRATION_0011); function statementsOf(file: string): string[] { return readFileSync(join(MIGRATIONS_DIR, file), "utf-8") @@ -93,8 +97,9 @@ function seedRegistry(raw: DatabaseSync) { } describe("migration 0011_release_tree_digest", () => { - it("is the latest migration", () => { - expect(migrationFiles.at(-1)).toBe(MIGRATION_0011); + it("exists and follows 0010", () => { + expect(migrationFiles).toContain(MIGRATION_0011); + expect(BEFORE_0011.at(-1)).toMatch(/^0010_/); }); it("contains only additive statements", () => { @@ -110,7 +115,7 @@ describe("migration 0011_release_tree_digest", () => { it("applies to a fresh database with the documented defaults", () => { const raw = new DatabaseSync(":memory:"); - applyMigrations(raw, migrationFiles); + applyMigrations(raw, THROUGH_0011); const byName = (table: string) => Object.fromEntries(columns(raw, table).map((c) => [c.name, c])); expect(byName("publishers").state).toMatchObject({ notnull: 1, dflt_value: "'active'" }); expect(byName("packages").visibility).toMatchObject({ notnull: 1, dflt_value: "'public'" }); @@ -131,7 +136,7 @@ describe("migration 0011_release_tree_digest", () => { it("keeps every existing row and value when applied to a database at 0010", () => { const raw = new DatabaseSync(":memory:"); - applyMigrations(raw, migrationFiles.slice(0, -1)); + applyMigrations(raw, BEFORE_0011); seedRegistry(raw); const columnsBefore = Object.fromEntries(REGISTRY_TABLES.map((t) => [t, columns(raw, t).map((c) => String(c.name))])); const before = snapshot(raw, columnsBefore); @@ -157,20 +162,33 @@ describe("migration 0011_release_tree_digest", () => { ).toEqual({ n: 3 }); }); - it("rolls back to the 0010 shape with the recorded SQL", () => { + it("rolls back to the 0010 shape with the recorded SQL, including wrangler's migration record", () => { const raw = new DatabaseSync(":memory:"); - applyMigrations(raw, migrationFiles.slice(0, -1)); + // The bookkeeping table wrangler d1 maintains next to the schema. + raw.exec( + "CREATE TABLE d1_migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL)", + ); + const record = raw.prepare("INSERT INTO d1_migrations (name) VALUES (?)"); + applyMigrations(raw, BEFORE_0011); + for (const file of BEFORE_0011) record.run(file); seedRegistry(raw); const shapeBefore = Object.fromEntries(REGISTRY_TABLES.map((t) => [t, [columns(raw, t), indexes(raw, t)]])); const columnsBefore = Object.fromEntries(REGISTRY_TABLES.map((t) => [t, columns(raw, t).map((c) => String(c.name))])); const rowsBefore = snapshot(raw, columnsBefore); + const appliedNames = () => (raw.prepare("SELECT name FROM d1_migrations ORDER BY id").all() as Array<{ name: string }>).map((r) => r.name); applyMigrations(raw, [MIGRATION_0011]); + record.run(MIGRATION_0011); const rollback = rollbackSql(MIGRATION_0011); expect(rollback.length).toBeGreaterThan(0); for (const statement of rollback) raw.exec(statement); expect(Object.fromEntries(REGISTRY_TABLES.map((t) => [t, [columns(raw, t), indexes(raw, t)]]))).toEqual(shapeBefore); expect(snapshot(raw, columnsBefore)).toEqual(rowsBefore); + expect(appliedNames()).toEqual(BEFORE_0011); + + // The rollback leaves the database ready to apply 0011 again. + applyMigrations(raw, [MIGRATION_0011]); + expect(columns(raw, "package_releases").map((c) => c.name)).toContain("release_digest"); }); }); From 5a95cf94dcd2f913c2a6b8a9c0ac106faaacee59 Mon Sep 17 00:00:00 2001 From: Duy Nguyen Date: Tue, 13 Oct 2026 12:42:00 +0700 Subject: [PATCH 14/14] fix(contracts): reject future-dated release statuses and pin reason classes Reject a signed release status whose issuedAt is more than 300 seconds ahead of the consumer clock with status.not_yet_valid, checked after the signature like expiry, with vectors at and beyond the skew edge. Classify reason format characters with the pinned Unicode 15.0 tables that the tree path rules use, so every runtime agrees on the verdict. Add manifest and status vectors whose signature has non-zero unused base64 bits, rejected by the signature pattern although lenient decoders return the original bytes. Document that consumers must not map these releases to publisher trust, the 24 hour re-sign cadence under the 7 day cap, the issuer obligation to advance issuedAt, and the two-step quarantine lift. --- docs/api-reference.md | 46 +++++++++++----- .../src/fixtures/release-manifest-v1.json | 7 ++- .../src/fixtures/release-status-v1.json | 28 +++++++++- packages/contracts/src/release-manifest.ts | 7 ++- .../src/release-status-vectors.test.ts | 16 +++++- packages/contracts/src/release-status.test.ts | 23 ++++++++ packages/contracts/src/release-status.ts | 55 +++++++++++++++---- .../contracts/src/release-vectors.test.ts | 16 +++++- .../contracts/src/test-support/vector-key.ts | 12 ++++ packages/skill-tree/src/rules.ts | 2 +- 10 files changed, 177 insertions(+), 35 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index d01fabf..b6c2883 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -754,7 +754,7 @@ members; unknown members (including `state`) are rejected at every level. | `files` | array | non-empty, at most 4096 entries of `{ path, mode, sha256, size }`, sorted by raw UTF-8 path bytes, no duplicates or case-fold collisions; `path` passes the tree path, NFC and noise rules; `mode` is `100644` or `100755`; `sha256` is bare lowercase hex; `size` at most 32 MiB | | `fileCount` | integer | equals `files.length` | | `treeSize` | integer | sum of file sizes, at most 256 MiB | -| `publisher` | object | `{ handle, verifiedBy }`; `handle` equals the identity publisher; `verifiedBy` is `none`, `github` or `admin` (`none` means not publisher-verified). The signature is a registry key, never a publisher key: AgentKit maps a verified registry signature with `verifiedBy` other than `none` to trust `registry-signed` (not `publisher-signed`), and `none` to `local-unverified` | +| `publisher` | object | `{ handle, verifiedBy }`; `handle` equals the identity publisher; `verifiedBy` is `none`, `github` or `admin` (`none` means not publisher-verified). The signature is a registry key, never a publisher key, so consumers MUST NOT map these releases to trust `publisher-signed`. AgentKit will expose a verified registry signature as `registry-signed` once its skill-lifecycle contract adds that value (bestagentkits/agentkit#2277); until then the AgentKit contract defines the mapping | | `sourceProvenance` | object or `null` | `{ repo, commit, path }`: https URL without credentials, full lowercase hex commit, package root path (`""` for the repository root). `null` when the release was not packed from a recorded git commit; consumers must label such a release "provenance unknown" | | `declaredScripts` | array | paths from `files`, unique and sorted like `files`; informational, never executed by SkillX | | `createdAt` | string | RFC 3339 UTC with whole seconds, e.g. `2026-10-13T04:30:00Z` | @@ -808,8 +808,10 @@ Cross-language vectors: `packages/contracts/src/fixtures/release-manifest-v1.jso (signed valid documents with their exact signing payload, tree serialization and file contents, including paths whose UTF-8 order differs from UTF-16 order; rejected documents with their single expected code: tampered, wrong -key, unknown key, Go-escaped, tree digest mismatch, case-variant member and -non-canonical byte variants; structure-only invalid-manifest cases). The +key, unknown key, Go-escaped, non-zero unused base64 bits in the signature +(`pattern`; a lenient decoder such as Go's `base64.StdEncoding` without +`.Strict()` accepts it, so verifiers must use the strict pattern), tree digest +mismatch, case-variant member and non-canonical byte variants; structure-only invalid-manifest cases). The vector key is derived from a public seed text and must never be trusted outside tests. @@ -825,7 +827,7 @@ base64). Source: `packages/contracts/src/release-status.ts`. | `releaseDigest` | string | `releaseDigest` of the manifest the status describes | | `manifestDigest` | string | `sha256:<64 lowercase hex>` of that manifest's signing payload (below); names exactly one release | | `state` | string | `draft`, `quarantined`, `published`, `yanked` | -| `reason` | string, optional | 1 to 512 UTF-8 bytes; no `Cc`, `Cf` (including every bidi control), `Zl`, `Zp` or `Cs` code points; omit the member when there is none (never `null` or `""`) | +| `reason` | string, optional | 1 to 512 UTF-8 bytes; no `Cc`, `Cf` (including every bidi control), `Zl`, `Zp` or `Cs` code points, classified with the same pinned Unicode 15.0 tables as the tree path rules (not the runtime's Unicode data); omit the member when there is none (never `null` or `""`) | | `issuedAt` | string | RFC 3339 UTC with whole seconds; not before the manifest's `createdAt` | | `expiresAt` | string | RFC 3339 UTC with whole seconds; after `issuedAt` and at most 7 days later | | `sequence` | integer | strictly increasing per `manifestDigest`, starting at 1; the status with sequence 1 is `draft` or `published` | @@ -842,16 +844,30 @@ exact bytes the manifest signature covers (`releaseManifestDigest`). It covers manifest digest, so a status never applies to a sibling release. **Freshness.** Every status expires. The registry re-signs the current status -of every release at least daily (a new `sequence`, same state), so a consumer -that fetches normally always holds an unexpired one. A consumer may keep using -an expired status only for content it already installed; an expired status -never authorizes a new resolution or install. +of every release every 24 hours (a new `sequence`, same state) with +`expiresAt = issuedAt + 24h`; the 7-day limit is a hard cap for the contract, +not the issuing cadence, and it is the worst-case staleness a consumer can be +held to. A consumer may keep using an expired status only for content it +already installed; an expired status never authorizes a new resolution or +install. A status whose `issuedAt` is more than 300 seconds ahead of the +consumer's clock is `status.not_yet_valid`, so a future-dated status can +neither outlive the lifetime bound nor, once accepted, lock out correctly +timed later statuses. + +**Issuer obligations.** Every new status of a release has a higher `sequence` +and an `issuedAt` at least 1 second later than the previous status (timestamps +have whole-second resolution; an issuer that yanks within the same second as a +re-sign bumps `issuedAt` by 1 second), otherwise consumers reject it as +`status.issued_at_regression`. **Transitions.** Between consecutive statuses of one release: `draft` to `draft` or `published`; `published` to `published`, `yanked` or `quarantined`; `yanked` to `yanked` or `published`; `quarantined` to `quarantined` or `yanked`. Nothing returns to `draft`. When sequences were skipped, the new state must be reachable in at most the sequence gap steps. +Lifting a quarantine therefore takes two signed steps, `quarantined` to +`yanked` to `published`; this is intended, so a quarantined release is never +republished by a single status. A consumer verifies the manifest first, then checks the received status document against it, in this order, and fails closed: @@ -866,7 +882,8 @@ document against it, in this order, and fails closed: 4. `manifestDigest` equals the verified manifest's manifest digest, else `status.manifest_mismatch`; 5. `issuedAt` is not before the manifest's `createdAt`, else `status.issued_before_release`; 6. the signature (`signature.unknown_key`, `signature.invalid_key`, `signature.mismatch`); -7. `now` is not after `expiresAt`, else `status.expired`; +7. `issuedAt` is at most 300 seconds after `now`, else `status.not_yet_valid`; + `now` is not after `expiresAt`, else `status.expired`; 8. against the last status it accepted for the same `manifestDigest`: a lower `sequence` is `status.rollback`; the same `sequence` with different signed bytes is `status.conflict` (identical bytes are accepted); a higher @@ -882,11 +899,12 @@ Cross-language vectors: `packages/contracts/src/fixtures/release-status-v1.json` under two versions; each vector names its manifest, the consumer's `now`, the previously accepted status document and the received document; valid statuses with their signing payload, including a daily re-sign, a -re-publication, a transition across a skipped sequence and a check exactly at -`expiresAt`; invalid documents with their single expected code, covering every -`status.*` code, tampering, wrong and unknown keys, Go escaping, non-canonical -bytes, a case-variant member, injected empty or `null` reasons and reason -rules). Same public test key as the manifest vectors. +re-publication, a transition across a skipped sequence, a check exactly at +`expiresAt` and an `issuedAt` exactly 300 seconds ahead of `now`; invalid +documents with their single expected code, covering every `status.*` code, +tampering, wrong and unknown keys, Go escaping, a signature with non-zero +unused base64 bits, non-canonical bytes, a case-variant member, injected empty +or `null` reasons and reason rules). Same public test key as the manifest vectors. ### Database columns (migration `0011_release_tree_digest`) diff --git a/packages/contracts/src/fixtures/release-manifest-v1.json b/packages/contracts/src/fixtures/release-manifest-v1.json index e58ebe1..c03ea60 100644 --- a/packages/contracts/src/fixtures/release-manifest-v1.json +++ b/packages/contracts/src/fixtures/release-manifest-v1.json @@ -3,7 +3,7 @@ "canonicalization": "signature = padded base64 Ed25519 over UTF-8 canonical JSON (RFC 8785 subset: members sorted by UTF-16 code units, no whitespace, non-negative safe integers only, only quote/backslash/C0 controls escaped, everything else raw UTF-8) of the manifest without its signature member; see packages/contracts/src/canonical-json.ts", "verifierRules": [ "parse document and re-encode it canonically; any byte difference (whitespace, member order, duplicate member, escapes, number form, BOM, trailing bytes, invalid UTF-8) is release.non_canonical", - "validate the structure; an unknown member, including one differing from a known member only by case, is unknown_field", + "validate the structure; an unknown member, including one differing from a known member only by case, is unknown_field; a signature not matching ^[A-Za-z0-9+/]{85}[AQgw]==$ (non-zero unused base64 bits included) is pattern", "require releaseDigest == 'sha256:' + hex SHA-256 of treeSerialization (agentkit.skill-tree/v1 of files), else release.tree_digest_mismatch", "verify the signature with the trusted key for keyId over the canonical JSON without signature: signature.unknown_key, signature.invalid_key or signature.mismatch" ], @@ -196,6 +196,11 @@ "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"9dEh1PX7CiqOi6gaClemhI+a0hF5HGSok+q6zFCsVwn2a3stv8J4WARheAWzRTIHJTwKBGT39lAgKNygoPgRAQ==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", "expectedCode": "signature.mismatch" }, + { + "name": "signature with non-zero unused base64 bits (same bytes for a lenient decoder)", + "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"eHdqtSvMKJH6rIGay9PgwrQhibz6H41zziDqgnIeWMw6rAlOS3NXVTgpVLeMRo5Kn8s6o7K8xs5V/UY29WHmDh==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", + "expectedCode": "pattern" + }, { "name": "releaseDigest is not the tree digest of files, correctly signed", "document": "{\"artifactDigest\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"artifactSize\":10240,\"channel\":\"stable\",\"createdAt\":\"2026-10-13T04:30:00Z\",\"declaredScripts\":[\"scripts/check.sh\"],\"fileCount\":3,\"files\":[{\"mode\":\"100644\",\"path\":\"SKILL.md\",\"sha256\":\"bcfdc953c1af6a7fdfde9d0a99d73bfba6a5fad8bb81d484b3bb416461ab5b11\",\"size\":102},{\"mode\":\"100644\",\"path\":\"references/café & notes
.md\",\"sha256\":\"d8bcfd787914776aba5f5923ce82d187239133d4c4fd46efd26c83aae0f0a84f\",\"size\":34},{\"mode\":\"100755\",\"path\":\"scripts/check.sh\",\"sha256\":\"b4d644d4279594903f1a9911956432d9473041f2984fc6014c14d7402c7d126c\",\"size\":18}],\"identity\":\"@publisher/review-guardrails\",\"keyId\":\"skillx-test-vector-1\",\"kind\":\"skill\",\"publisher\":{\"handle\":\"publisher\",\"verifiedBy\":\"github\"},\"releaseDigest\":\"sha256:2b80424f6839c7ae445108317a623797ecab4ee94fbb6cddbc9695326aed8544\",\"schemaVersion\":\"skillx.release/v1\",\"signature\":\"3f37segS8H/FZ+TY7GG+T0dObWAbq/6zEHl3yJfGVzNviuHyNvikJym/MIn6jkEu4Keg9iCeDzj30lzLRaXbCA==\",\"signatureAlgorithm\":\"ed25519\",\"sourceProvenance\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"path\":\"skills/review-guardrails\",\"repo\":\"https://github.com/publisher/review-guardrails\"},\"treeSize\":154,\"version\":\"1.2.0\"}", diff --git a/packages/contracts/src/fixtures/release-status-v1.json b/packages/contracts/src/fixtures/release-status-v1.json index 4a911db..c90b0ef 100644 --- a/packages/contracts/src/fixtures/release-status-v1.json +++ b/packages/contracts/src/fixtures/release-status-v1.json @@ -5,11 +5,12 @@ "consumerRules": [ "verify the manifest document first (skillx.release/v1 vectors); statuses are checked against that verified manifest", "re-encode the received document canonically; any byte difference is status.non_canonical", - "validate the structure: unknown member unknown_field; missing member required; state outside draft|quarantined|published|yanked status.unknown_state even when signed; reason 1..512 UTF-8 bytes (length, null is type) without Cc, Cf (incl. bidi controls), Zl, Zp or Cs code points (format); issuedAt < expiresAt <= issuedAt + 7 days else status.invalid_lifetime; sequence 1 must be draft or published else status.invalid_transition", + "validate the structure: unknown member unknown_field; missing member required; state outside draft|quarantined|published|yanked status.unknown_state even when signed; reason 1..512 UTF-8 bytes (length, null is type) without Cc, Cf (incl. bidi controls, Unicode 15.0 tables), Zl, Zp or Cs code points (format); signature matching ^[A-Za-z0-9+/]{85}[AQgw]==$ else pattern; issuedAt < expiresAt <= issuedAt + 7 days else status.invalid_lifetime; sequence 1 must be draft or published else status.invalid_transition", "require releaseDigest to equal the verified manifest's releaseDigest, else status.digest_mismatch", "require manifestDigest to equal the verified manifest's manifest digest, else status.manifest_mismatch", "require issuedAt >= the manifest createdAt, else status.issued_before_release", "verify the signature with the trusted key for keyId: signature.unknown_key, signature.invalid_key or signature.mismatch", + "require issuedAt <= now + 300 s, else status.not_yet_valid", "require now <= expiresAt, else status.expired; an expired status may only keep already-installed content, never authorize a new resolution or install", "per manifestDigest, against the last accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict (identical bytes are accepted); a higher sequence needs a later issuedAt (status.issued_at_regression) and a state reachable in at most (sequence gap) steps of draft->draft|published, published->published|yanked|quarantined, yanked->yanked|published, quarantined->quarantined|yanked (status.invalid_transition)" ], @@ -109,6 +110,15 @@ "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", "signingPayload": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", "signingPayloadSha256": "530fa7fca53abb9f7f68fcf122fcc0e05ef5bd50b91afb004a2ab9604efd3583" + }, + { + "name": "issuedAt exactly at the 300 second clock-skew limit", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T04:26:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayload": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "signingPayloadSha256": "530fa7fca53abb9f7f68fcf122fcc0e05ef5bd50b91afb004a2ab9604efd3583" } ], "invalid": [ @@ -344,6 +354,22 @@ "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", "expectedCode": "status.expired" }, + { + "name": "issuedAt 301 seconds ahead of the consumer's clock", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-13T04:25:59Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-14T04:31:00Z\",\"issuedAt\":\"2026-10-13T04:31:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":1,\"signature\":\"R32/j4L6Tn/tlB1TV9X5EI0Yu1F82aAV8NZMEG5s0yPFqy3/T+n8nwNuVdRrGVhMCC1eBKnuPebWVNLcGQ4uDg==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"published\"}", + "expectedCode": "status.not_yet_valid" + }, + { + "name": "signature with non-zero unused base64 bits (same bytes for a lenient decoder)", + "manifest": "1.2.0 of the multi-file tree", + "now": "2026-10-14T12:00:00Z", + "previous": null, + "document": "{\"expiresAt\":\"2026-10-15T09:00:00Z\",\"issuedAt\":\"2026-10-14T09:00:00Z\",\"keyId\":\"skillx-test-vector-1\",\"manifestDigest\":\"sha256:64de9b194abf1627ca00c09be146106c3c044637eeba2c27fb031d5b171312bc\",\"reason\":\"Yanked: unsafe script & café notes\",\"releaseDigest\":\"sha256:4c2eac09c7ba5fc5e0d642959c7e38b65a53ca08e65b8e728d6ca5798e458974\",\"schemaVersion\":\"skillx.release-status/v1\",\"sequence\":2,\"signature\":\"QrSknNYk/lisL8cn+8RGzYvSqD/5bCEUmAnR3RYdGqG/cefvgXcTx/U5Eg+c+2Pk/7VVzTu4FlTahxSewOUABB==\",\"signatureAlgorithm\":\"ed25519\",\"state\":\"yanked\"}", + "expectedCode": "pattern" + }, { "name": "lower sequence than the accepted status (rollback)", "manifest": "1.2.0 of the multi-file tree", diff --git a/packages/contracts/src/release-manifest.ts b/packages/contracts/src/release-manifest.ts index adb35f2..59925b3 100644 --- a/packages/contracts/src/release-manifest.ts +++ b/packages/contracts/src/release-manifest.ts @@ -80,9 +80,10 @@ export type ReleaseFileMode = (typeof RELEASE_FILE_MODES)[number]; /** * How SkillX verified that the publisher owns its handle. The signature is a - * registry key, never a publisher key: AgentKit maps a verified registry - * signature with `verifiedBy != "none"` to trust `registry-signed` (not - * `publisher-signed`), and `none` to `local-unverified`. + * registry key, never a publisher key, so consumers MUST NOT map these + * releases to trust `publisher-signed`. AgentKit will expose a verified + * registry signature as `registry-signed` once its skill-lifecycle contract + * adds that value; until then its trust mapping is defined there. */ export const PUBLISHER_VERIFICATIONS = ["none", "github", "admin"] as const; export type PublisherVerification = (typeof PUBLISHER_VERIFICATIONS)[number]; diff --git a/packages/contracts/src/release-status-vectors.test.ts b/packages/contracts/src/release-status-vectors.test.ts index 98a812b..1ae1284 100644 --- a/packages/contracts/src/release-status-vectors.test.ts +++ b/packages/contracts/src/release-status-vectors.test.ts @@ -32,6 +32,7 @@ import { hex, sha256, vectorKey, + withNonZeroPaddingBits, writeVectorFile, writeVectorsRequested, } from "./test-support/vector-key"; @@ -75,6 +76,8 @@ const MINIMAL = "minimal release"; const DAY_ONE = "2026-10-13T12:00:00Z"; const DAY_TWO = "2026-10-14T12:00:00Z"; const DAY_THREE = "2026-10-15T12:00:00Z"; +/** 300 seconds before the published fixture's issuedAt (2026-10-13T04:31:00Z). */ +const SKEW_EDGE = "2026-10-13T04:26:00Z"; /** One day after the yanked fixture: the next daily re-sign. */ const NEXT_DAY = { issuedAt: "2026-10-15T09:00:00Z", expiresAt: "2026-10-16T09:00:00Z" }; @@ -131,6 +134,7 @@ async function buildVectors(): Promise { DAY_THREE, ], ["checked exactly at expiresAt", published, null, V120, RELEASE_STATUS_PUBLISHED_FIXTURE.expiresAt], + ["issuedAt exactly at the 300 second clock-skew limit", published, null, V120, SKEW_EDGE], ]; for (const [name, status, previous, manifest, now] of validCases) { const payload = releaseStatusSigningPayload(status); @@ -245,6 +249,12 @@ async function buildVectors(): Promise { { now: DAY_ONE }, ); add("expired one second after expiresAt", doc(published), C.expired, { now: "2026-10-14T04:31:01Z" }); + add("issuedAt 301 seconds ahead of the consumer's clock", doc(published), C.notYetValid, { now: "2026-10-13T04:25:59Z" }); + add( + "signature with non-zero unused base64 bits (same bytes for a lenient decoder)", + tamper((s) => void (s.signature = withNonZeroPaddingBits(s.signature))), + "pattern", + ); // Against the previous accepted status. add("lower sequence than the accepted status (rollback)", doc(published), C.rollback, { previous: yanked, now: DAY_ONE }); @@ -273,11 +283,12 @@ async function buildVectors(): Promise { consumerRules: [ "verify the manifest document first (skillx.release/v1 vectors); statuses are checked against that verified manifest", "re-encode the received document canonically; any byte difference is status.non_canonical", - "validate the structure: unknown member unknown_field; missing member required; state outside draft|quarantined|published|yanked status.unknown_state even when signed; reason 1..512 UTF-8 bytes (length, null is type) without Cc, Cf (incl. bidi controls), Zl, Zp or Cs code points (format); issuedAt < expiresAt <= issuedAt + 7 days else status.invalid_lifetime; sequence 1 must be draft or published else status.invalid_transition", + "validate the structure: unknown member unknown_field; missing member required; state outside draft|quarantined|published|yanked status.unknown_state even when signed; reason 1..512 UTF-8 bytes (length, null is type) without Cc, Cf (incl. bidi controls, Unicode 15.0 tables), Zl, Zp or Cs code points (format); signature matching ^[A-Za-z0-9+/]{85}[AQgw]==$ else pattern; issuedAt < expiresAt <= issuedAt + 7 days else status.invalid_lifetime; sequence 1 must be draft or published else status.invalid_transition", "require releaseDigest to equal the verified manifest's releaseDigest, else status.digest_mismatch", "require manifestDigest to equal the verified manifest's manifest digest, else status.manifest_mismatch", "require issuedAt >= the manifest createdAt, else status.issued_before_release", "verify the signature with the trusted key for keyId: signature.unknown_key, signature.invalid_key or signature.mismatch", + "require issuedAt <= now + 300 s, else status.not_yet_valid", "require now <= expiresAt, else status.expired; an expired status may only keep already-installed content, never authorize a new resolution or install", "per manifestDigest, against the last accepted status: a lower sequence is status.rollback; the same sequence with different signed bytes is status.conflict (identical bytes are accepted); a higher sequence needs a later issuedAt (status.issued_at_regression) and a state reachable in at most (sequence gap) steps of draft->draft|published, published->published|yanked|quarantined, yanked->yanked|published, quarantined->quarantined|yanked (status.invalid_transition)", ], @@ -326,7 +337,7 @@ describe("skillx.release-status/v1 cross-language vectors", () => { }); it.skipIf(writeVectorsRequested)("accept every valid vector", async () => { - expect(committed.valid.length).toBe(8); + expect(committed.valid.length).toBe(9); for (const vector of committed.valid) { expect(await verifyVector(vector), vector.name).toEqual({ valid: true, issues: [] }); const status = JSON.parse(vector.document) as SkillxReleaseStatus; @@ -350,6 +361,7 @@ describe("skillx.release-status/v1 cross-language vectors", () => { "length", "type", "format", + "pattern", ].sort(), ); }); diff --git a/packages/contracts/src/release-status.test.ts b/packages/contracts/src/release-status.test.ts index e397391..6c5b693 100644 --- a/packages/contracts/src/release-status.test.ts +++ b/packages/contracts/src/release-status.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { UNICODE_VERSION } from "@skillx/skill-tree/rules"; import { canonicalJson } from "./canonical-json"; import { RELEASE_FIXTURE_KEY_ID, RELEASE_MANIFEST_FIXTURE, RELEASE_MANIFEST_MINIMAL_FIXTURE } from "./fixtures/release-fixtures"; import { @@ -157,6 +158,13 @@ describe("skillx.release-status/v1 validation", () => { expect(codes(validateUnsignedReleaseStatus(status))).toEqual(["format"]); }); + it("classifies format characters with the pinned Unicode 15.0 tables", () => { + expect(UNICODE_VERSION).toBe("15.0.0"); + // U+13430 EGYPTIAN HIEROGLYPH VERTICAL JOINER is Cf in Unicode 15.0. + const status = { ...RELEASE_STATUS_YANKED_FIXTURE, reason: `x${String.fromCodePoint(0x13430)}y` }; + expect(codes(validateUnsignedReleaseStatus(status))).toEqual(["format"]); + }); + it("accepts non-ASCII letters and emoji in the reason", () => { const status = { ...RELEASE_STATUS_YANKED_FIXTURE, reason: `caf${String.fromCodePoint(0xe9)} ${String.fromCodePoint(0x1f600)}` }; expect(validateUnsignedReleaseStatus(status)).toEqual(OK); @@ -318,6 +326,21 @@ describe("skillx.release-status/v1 verification", () => { }); }); + describe("clock skew", () => { + it("accepts an issuedAt up to 300 seconds ahead of now", async () => { + const { keys, published, at } = await setup(); + expect(await verifyReleaseStatus(doc(published), keys, at(new Date("2026-10-13T04:26:00Z")))).toEqual(OK); + }); + + it("rejects an issuedAt further ahead, but only once the signature verifies", async () => { + const { keys, published, at } = await setup(); + const early = at(new Date("2026-10-13T04:25:59Z")); + expect(codes(await verifyReleaseStatus(doc(published), keys, early))).toEqual([RELEASE_STATUS_CODES.notYetValid]); + const forged = { ...clone(published), state: "draft" }; + expect(codes(await verifyReleaseStatus(doc(forged), keys, early))).toEqual([RELEASE_SIGNATURE_CODES.mismatch]); + }); + }); + it("rejects a status issued before the release manifest was created", async () => { const { keys, sign, at } = await setup(); const early = await sign({ state: "published", sequence: 1, issuedAt: "2026-10-13T04:29:59Z", expiresAt: "2026-10-14T04:29:59Z" }); diff --git a/packages/contracts/src/release-status.ts b/packages/contracts/src/release-status.ts index 0b082a9..f3d5cb8 100644 --- a/packages/contracts/src/release-status.ts +++ b/packages/contracts/src/release-status.ts @@ -9,12 +9,15 @@ * (`releaseDigest`) may be released under several identities or versions. * * Freshness: every status expires. `expiresAt` is after `issuedAt` and at - * most `RELEASE_STATUS_MAX_LIFETIME_SECONDS` (7 days) later; the registry - * re-signs the current status at least daily, so a consumer that fetches - * normally always holds an unexpired one. A verifier rejects a status whose - * `expiresAt` is before the caller's `now` (`status.expired`). A consumer may - * keep using an expired status only for content it already installed, never - * to authorize a new resolution or install. + * most `RELEASE_STATUS_MAX_LIFETIME_SECONDS` (7 days, a hard cap) later; the + * registry re-signs the current status every 24 hours with + * `expiresAt = issuedAt + 24h`, so a consumer that fetches normally always + * holds an unexpired one. A verifier rejects a status whose `expiresAt` is + * before the caller's `now` (`status.expired`) or whose `issuedAt` is more + * than `RELEASE_STATUS_MAX_CLOCK_SKEW_SECONDS` after it + * (`status.not_yet_valid`). A consumer may keep using an expired status only + * for content it already installed, never to authorize a new resolution or + * install. * * Each new status for a release carries a strictly higher `sequence`, tracked * per `manifestDigest`, so a consumer that keeps the last status it accepted @@ -34,6 +37,7 @@ * options (`manifest`, `now`, `previous`) throw `ReleaseManifestError`. */ +import { isFormat } from "@skillx/skill-tree/rules"; import { canonicalJsonBytes } from "./canonical-json"; import { RELEASE_STATES, isReleaseState } from "./package-manifest"; import type { ReleaseState } from "./package-manifest"; @@ -61,6 +65,13 @@ export const SKILLX_RELEASE_STATUS_SCHEMA_VERSION = "skillx.release-status/v1"; export const RELEASE_STATUS_REASON_MAX_BYTES = 512; /** Maximum `expiresAt - issuedAt`: 7 days. */ export const RELEASE_STATUS_MAX_LIFETIME_SECONDS = 7 * 24 * 60 * 60; +/** + * How far `issuedAt` may lie ahead of the consumer's clock. A status issued + * further in the future is `status.not_yet_valid`: it would otherwise stay + * valid past the lifetime bound and, once accepted as `previous`, make every + * correctly timed later status an `issuedAt` regression. + */ +export const RELEASE_STATUS_MAX_CLOCK_SKEW_SECONDS = 300; /** Status-specific failure codes, in addition to the signature codes. */ export const RELEASE_STATUS_CODES = { @@ -70,6 +81,7 @@ export const RELEASE_STATUS_CODES = { digestMismatch: "status.digest_mismatch", manifestMismatch: "status.manifest_mismatch", issuedBeforeRelease: "status.issued_before_release", + notYetValid: "status.not_yet_valid", expired: "status.expired", rollback: "status.rollback", conflict: "status.conflict", @@ -140,8 +152,22 @@ const STATUS_FIELDS = [ ] as const; const OPTIONAL_STATUS_FIELDS = ["reason"] as const; const TIMESTAMP_FORMAT = "must be an RFC 3339 UTC timestamp with whole seconds, e.g. 2026-10-13T04:30:00Z"; -/** Controls, format characters (including every bidi control), line and paragraph separators, surrogates. */ -const FORBIDDEN_REASON_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cs}]/u; +/** + * Whether a `reason` code point is forbidden: Cc (C0, DEL, C1), Cf (every + * bidi control included) from the pinned Unicode 15.0 tables that path rules + * use, Zl (U+2028), Zp (U+2029) and Cs (surrogates). The pinned table keeps + * the result independent of the runtime's Unicode version. + */ +function forbiddenReasonCodePoint(cp: number): boolean { + return ( + cp < 0x20 || + (cp >= 0x7f && cp <= 0x9f) || + cp === 0x2028 || + cp === 0x2029 || + (cp >= 0xd800 && cp <= 0xdfff) || + isFormat(cp) + ); +} /** Longest chain of distinct transitions; a larger sequence gap allows nothing more. */ const MAX_TRANSITION_STEPS = 4; @@ -153,7 +179,7 @@ function validateReason(reason: unknown, issues: ValidationIssue[]): void { const bytes = new TextEncoder().encode(reason).length; if (bytes === 0 || bytes > RELEASE_STATUS_REASON_MAX_BYTES) { issues.push(issue("reason", "length", `must be 1 to ${RELEASE_STATUS_REASON_MAX_BYTES} UTF-8 bytes`)); - } else if (FORBIDDEN_REASON_CHARACTERS.test(reason)) { + } else if (Array.from(reason).some((c) => forbiddenReasonCodePoint(c.codePointAt(0)!))) { issues.push(issue("reason", "format", "must not contain control, format, bidi or line separator characters")); } } @@ -292,8 +318,8 @@ function checkPrevious( * canonical bytes (`status.non_canonical`), structure (including * `status.unknown_state`, `status.invalid_lifetime` and the first-status * rule), `status.digest_mismatch`, `status.manifest_mismatch`, - * `status.issued_before_release`, the signature, `status.expired` against - * `options.now`, then, with `options.previous`, `status.rollback`, + * `status.issued_before_release`, the signature, `status.not_yet_valid` and + * `status.expired` against `options.now`, then, with `options.previous`, `status.rollback`, * `status.conflict`, `status.issued_at_regression` and * `status.invalid_transition`. Never throws for `document`; invalid options * throw `ReleaseManifestError`. @@ -326,6 +352,13 @@ export async function verifyReleaseStatus( const payload = releaseStatusSigningPayload(status); const failure = await verifyPayload(payload, status.signature, status.keyId, publicKeys); if (failure !== null) return validationFail([failure]); + if (timestampMs(status.issuedAt) > now + RELEASE_STATUS_MAX_CLOCK_SKEW_SECONDS * 1000) { + return fail( + "issuedAt", + RELEASE_STATUS_CODES.notYetValid, + `is more than ${RELEASE_STATUS_MAX_CLOCK_SKEW_SECONDS} seconds after the consumer's clock`, + ); + } if (now > timestampMs(status.expiresAt)) { return fail("expiresAt", RELEASE_STATUS_CODES.expired, `expired at ${status.expiresAt}`); } diff --git a/packages/contracts/src/release-vectors.test.ts b/packages/contracts/src/release-vectors.test.ts index 6a89fb0..57734b5 100644 --- a/packages/contracts/src/release-vectors.test.ts +++ b/packages/contracts/src/release-vectors.test.ts @@ -34,6 +34,7 @@ import { hex, sha256, vectorKey, + withNonZeroPaddingBits, writeVectorFile, writeVectorsRequested, } from "./test-support/vector-key"; @@ -162,7 +163,7 @@ async function buildVectors(): Promise { "signature = padded base64 Ed25519 over UTF-8 canonical JSON (RFC 8785 subset: members sorted by UTF-16 code units, no whitespace, non-negative safe integers only, only quote/backslash/C0 controls escaped, everything else raw UTF-8) of the manifest without its signature member; see packages/contracts/src/canonical-json.ts", verifierRules: [ "parse document and re-encode it canonically; any byte difference (whitespace, member order, duplicate member, escapes, number form, BOM, trailing bytes, invalid UTF-8) is release.non_canonical", - "validate the structure; an unknown member, including one differing from a known member only by case, is unknown_field", + "validate the structure; an unknown member, including one differing from a known member only by case, is unknown_field; a signature not matching ^[A-Za-z0-9+/]{85}[AQgw]==$ (non-zero unused base64 bits included) is pattern", "require releaseDigest == 'sha256:' + hex SHA-256 of treeSerialization (agentkit.skill-tree/v1 of files), else release.tree_digest_mismatch", "verify the signature with the trusted key for keyId over the canonical JSON without signature: signature.unknown_key, signature.invalid_key or signature.mismatch", ], @@ -186,6 +187,11 @@ async function buildVectors(): Promise { document: tamper((m) => void (m.signature = goStyleSignature)), expectedCode: mismatch, }, + { + name: "signature with non-zero unused base64 bits (same bytes for a lenient decoder)", + document: tamper((m) => void (m.signature = withNonZeroPaddingBits(m.signature))), + expectedCode: "pattern", + }, { name: "releaseDigest is not the tree digest of files, correctly signed", document: canonicalJson(wrongTree), @@ -273,7 +279,13 @@ describe("skillx.release/v1 cross-language vectors", () => { codes.add(vector.expectedCode); } expect([...codes].sort()).toEqual( - [...Object.values(RELEASE_MANIFEST_CODES), RELEASE_SIGNATURE_CODES.mismatch, RELEASE_SIGNATURE_CODES.unknownKey, "unknown_field"].sort(), + [ + ...Object.values(RELEASE_MANIFEST_CODES), + RELEASE_SIGNATURE_CODES.mismatch, + RELEASE_SIGNATURE_CODES.unknownKey, + "unknown_field", + "pattern", + ].sort(), ); }); diff --git a/packages/contracts/src/test-support/vector-key.ts b/packages/contracts/src/test-support/vector-key.ts index 80d8a49..709924c 100644 --- a/packages/contracts/src/test-support/vector-key.ts +++ b/packages/contracts/src/test-support/vector-key.ts @@ -63,6 +63,18 @@ export function goJsonStyle(payload: Uint8Array): Uint8Array { return encoder.encode(text); } +/** + * The same signature bytes with non-zero unused bits in the last base64 data + * character: lenient decoders (atob, Go base64.StdEncoding) decode it to the + * original bytes, the strict signature pattern rejects it. + */ +export function withNonZeroPaddingBits(signature: string): string { + const last = signature.charAt(85); + const next = { A: "B", Q: "R", g: "h", w: "x" }[last]; + if (next === undefined) throw new Error(`unexpected final signature character ${last}`); + return `${signature.slice(0, 85)}${next}==`; +} + /** Writes `data` as pretty JSON next to this package's fixtures (only when regenerating vectors). */ export async function writeVectorFile(fileName: string, data: unknown): Promise { // A computed specifier keeps Node typings out of the Worker typecheck that also covers this file. diff --git a/packages/skill-tree/src/rules.ts b/packages/skill-tree/src/rules.ts index 646744f..72fa641 100644 --- a/packages/skill-tree/src/rules.ts +++ b/packages/skill-tree/src/rules.ts @@ -10,7 +10,7 @@ export type { SkillTreeErrorCode } from "./errors"; export { isNoisePath, validatePath } from "./path-rules"; export { checkCollisions, foldKey } from "./collision"; -export { UNICODE_VERSION } from "./unicode-15"; +export { UNICODE_VERSION, isFormat } from "./unicode-15"; export { IDENTITY_PATTERN, RESERVED_PUBLISHER, checkPublisher, validateIdentity } from "./identity";