typesea 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to TypeSea are recorded here.
4
4
 
5
+ ## 0.3.1 - Unreleased
6
+
7
+ ### Changed
8
+
9
+ - Hardened the manual GitHub Release workflow so `workflow_dispatch` tag input
10
+ is passed through environment variables and validated as a release tag before
11
+ it reaches shell output.
12
+ - Added `SECURITY.md` with supported versions, reporting guidance, and the
13
+ security boundary for safe, unsafe, unchecked, AOT, and dynamic compilation.
14
+ - Added a post-publish npm registry verification step to the GitHub Publish
15
+ workflow.
16
+ - Added `release:publish` so the repository-owned publish command always uses
17
+ `npm publish --provenance --access public --ignore-scripts`.
18
+ - Removed the version-pinned Socket badge URL from the README.
19
+ - Refreshed the benchmark snapshot and docs graph from the 2026-07-05 local
20
+ `bench/ecosystem.bench.ts` run.
21
+ - Clarified the release path: local npm publishing is allowed for emergency
22
+ manual releases, but normal releases should go through GitHub Release so npm
23
+ provenance is attached.
24
+ - Expanded decoder documentation around method chaining with `transform`,
25
+ `default`, `prefault`, and `catch`.
26
+
5
27
  ## 0.3.0 - 2026-07-05
6
28
 
7
29
  ### Added
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # TypeSea
2
2
 
3
3
  [![CI](https://github.com/Feralthedogg/TypeSea/actions/workflows/ci.yml/badge.svg)](https://github.com/Feralthedogg/TypeSea/actions/workflows/ci.yml)
4
- [![Socket Badge](https://badge.socket.dev/npm/package/typesea/0.3.0)](https://badge.socket.dev/npm/package/typesea/0.3.0)
4
+ [![Socket Badge](https://badge.socket.dev/npm/package/typesea)](https://socket.dev/npm/package/typesea)
5
5
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
6
6
  ![TypeScript](https://img.shields.io/badge/language-TypeScript-informational)
7
7
  ![Dependencies](https://img.shields.io/badge/runtime%20deps-zero-brightgreen)
@@ -17,7 +17,7 @@ runtime compilation, and AOT source generation.
17
17
 
18
18
  ## Benchmark Headline
19
19
 
20
- Last local benchmark on 2026-07-04 KST:
20
+ Last local benchmark on 2026-07-05 KST:
21
21
  `npm run bench -- bench/ecosystem.bench.ts --run`, strict-object contract,
22
22
  operations per second on one machine.
23
23
 
@@ -40,6 +40,12 @@ loads, allocation-light strict-key loops, and V8-friendly monomorphic codegen.
40
40
  > return frozen `Result` values — `any`, `try`, and `catch` are banned from the
41
41
  > entire codebase and enforced by policy gates.
42
42
 
43
+ > [!WARNING]
44
+ > `unsafe` and `unchecked` are **not public-boundary modes**. They are for
45
+ > trusted, already-normalized data where the caller accepts getter execution,
46
+ > prototype-backed values, and weaker strict-extra-key guarantees. Use the
47
+ > default safe mode for external input.
48
+
43
49
  ---
44
50
 
45
51
  ## Why
@@ -222,50 +228,50 @@ failed check() -> schema-aware diagnostic collector
222
228
 
223
229
  ## Performance Snapshot
224
230
 
225
- Last local benchmark on 2026-07-04 KST, using
231
+ Last local benchmark on 2026-07-05 KST, using
226
232
  `npm run bench -- bench/ecosystem.bench.ts --run` on the benchmark strict-object
227
233
  contract. These are operations per second on one machine, not release
228
234
  guarantees.
229
235
 
230
236
  | Valid object path | hz |
231
237
  | --- | ---: |
232
- | TypeSea interpreted `is()` | 513,701 |
233
- | TypeSea compiled safe `is()` | 4,297,306 |
234
- | TypeSea compiled unsafe `is()` | 36,297,653 |
235
- | TypeSea compiled unchecked `is()` | 42,581,174 |
236
- | Zod `safeParse` | 1,343,756 |
237
- | Valibot `safeParse` | 1,406,528 |
238
- | Ajv compiled | 4,275,389 |
238
+ | TypeSea interpreted `is()` | 478,576 |
239
+ | TypeSea compiled safe `is()` | 5,109,602 |
240
+ | TypeSea compiled unsafe `is()` | 36,777,097 |
241
+ | TypeSea compiled unchecked `is()` | 42,620,570 |
242
+ | Zod `safeParse` | 1,400,045 |
243
+ | Valibot `safeParse` | 1,400,599 |
244
+ | Ajv compiled | 4,238,036 |
239
245
 
240
246
  | Valid diagnostic path | hz |
241
247
  | --- | ---: |
242
- | TypeSea interpreted `check()` | 503,232 |
243
- | TypeSea compiled safe `check()` | 3,903,929 |
244
- | TypeSea compiled unsafe `check()` | 35,568,425 |
245
- | TypeSea compiled unchecked `check()` | 40,084,605 |
246
- | Zod `safeParse` | 1,355,014 |
247
- | Valibot `safeParse` | 1,378,266 |
248
- | Ajv compiled | 4,278,587 |
248
+ | TypeSea interpreted `check()` | 424,989 |
249
+ | TypeSea compiled safe `check()` | 4,642,948 |
250
+ | TypeSea compiled unsafe `check()` | 37,184,199 |
251
+ | TypeSea compiled unchecked `check()` | 42,487,325 |
252
+ | Zod `safeParse` | 1,278,859 |
253
+ | Valibot `safeParse` | 1,391,040 |
254
+ | Ajv compiled | 4,338,063 |
249
255
 
250
256
  | Invalid object path | hz |
251
257
  | --- | ---: |
252
- | TypeSea interpreted `is()` | 3,636,369 |
253
- | TypeSea compiled safe `is()` | 42,080,241 |
254
- | TypeSea compiled unsafe `is()` | 49,654,076 |
255
- | TypeSea compiled unchecked `is()` | 50,482,732 |
256
- | Zod `safeParse` | 84,272 |
257
- | Valibot `safeParse` | 878,521 |
258
- | Ajv compiled | 27,820,643 |
258
+ | TypeSea interpreted `is()` | 3,325,603 |
259
+ | TypeSea compiled safe `is()` | 43,094,061 |
260
+ | TypeSea compiled unsafe `is()` | 50,738,235 |
261
+ | TypeSea compiled unchecked `is()` | 50,898,012 |
262
+ | Zod `safeParse` | 84,647 |
263
+ | Valibot `safeParse` | 866,013 |
264
+ | Ajv compiled | 30,535,761 |
259
265
 
260
266
  | Invalid diagnostic path | hz |
261
267
  | --- | ---: |
262
- | TypeSea interpreted `check()` | 420,446 |
263
- | TypeSea compiled safe `check()` | 2,086,129 |
264
- | TypeSea compiled unsafe `check()` | 3,077,367 |
265
- | TypeSea compiled unchecked `check()` | 3,673,508 |
266
- | Zod `safeParse` | 79,613 |
267
- | Valibot `safeParse` | 887,991 |
268
- | Ajv compiled | 28,713,035 |
268
+ | TypeSea interpreted `check()` | 405,590 |
269
+ | TypeSea compiled safe `check()` | 2,107,460 |
270
+ | TypeSea compiled unsafe `check()` | 3,186,702 |
271
+ | TypeSea compiled unchecked `check()` | 3,509,673 |
272
+ | Zod `safeParse` | 85,355 |
273
+ | Valibot `safeParse` | 788,870 |
274
+ | Ajv compiled | 29,951,403 |
269
275
 
270
276
  The safe compiled path stays close to Ajv while retaining TypeSea hostile-input
271
277
  semantics: descriptor-based property reads, symbol/non-enumerable strict-key
@@ -407,6 +413,7 @@ npm run check:consumer # tarball install + runtime/type smoke in a temp project
407
413
  npm run bench -- --run # benchmark smoke
408
414
  npm run pack:dry # package contents dry run
409
415
  npm run release:check # the full pre-publish gate (everything above)
416
+ npm run release:publish # npm publish with provenance and ignored lifecycle scripts
410
417
  ```
411
418
 
412
419
  `npm run release:check` runs the same gate expected before publishing:
@@ -414,6 +421,16 @@ typecheck, lint, tests, build, docs smoke, dist policy, public API snapshot,
414
421
  package contents, consumer install, benchmark smoke, and pack dry run.
415
422
  CI executes it on Node 20.19, 22, and 24; releases publish with npm provenance.
416
423
 
424
+ Release path:
425
+
426
+ 1. Push a `vX.Y.Z` tag or run the GitHub `Release` workflow with that tag.
427
+ 2. The release workflow verifies that the tag matches `package.json`.
428
+ 3. Publishing happens from the GitHub `Publish` workflow through `npm run release:publish`, which expands to `npm publish --provenance --access public --ignore-scripts`.
429
+
430
+ Local publishing with `NPM_TOKEN` is reserved for manual recovery releases. It
431
+ must still run `npm run release:check` first, and it cannot attach GitHub OIDC
432
+ provenance.
433
+
417
434
  > [!NOTE]
418
435
  > Benchmark comparison packages (Zod, Valibot, Ajv) are dev dependencies only —
419
436
  > package policy rejects them from every runtime dependency field. The
@@ -427,6 +444,18 @@ CI executes it on Node 20.19, 22, and 24; releases publish with npm provenance.
427
444
  - [Documentation site](https://feralthedogg.github.io/TypeSea/)
428
445
  - [API reference](docs/api.md)
429
446
  - [Engine notes](docs/engine-notes.md)
447
+ - [Security policy](https://github.com/Feralthedogg/TypeSea/blob/main/SECURITY.md)
448
+
449
+ ---
450
+
451
+ ## Migration Notes
452
+
453
+ ### 0.3.0 to 0.3.1
454
+
455
+ No application code changes are required. `0.3.1` is a release-hardening patch:
456
+ it tightens manual release tag handling, documents npm provenance expectations,
457
+ adds a security policy, and verifies that npm exposes the published version after
458
+ the GitHub publish workflow completes.
430
459
 
431
460
  ---
432
461
 
package/SECURITY.md ADDED
@@ -0,0 +1,52 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ TypeSea supports the latest published minor line. Security fixes are released as
6
+ patch versions whenever a fix can be shipped without changing the public API.
7
+
8
+ | Version | Supported |
9
+ | --- | --- |
10
+ | 0.3.x | yes |
11
+ | 0.2.x | no |
12
+
13
+ ## Reporting A Vulnerability
14
+
15
+ Please report security issues through GitHub Security Advisories for
16
+ `Feralthedogg/TypeSea`. If that is unavailable, open a GitHub issue with the
17
+ minimum public detail needed to start coordination and mark the title as a
18
+ security report.
19
+
20
+ Useful reports include:
21
+
22
+ - affected TypeSea version
23
+ - minimal schema and input needed to reproduce the issue
24
+ - whether the issue affects `safe`, `unsafe`, `unchecked`, AOT, JSON Schema, or
25
+ an adapter
26
+ - expected verdict and actual verdict
27
+ - generated source or stack output that helps reproduce the issue
28
+
29
+ ## Security Boundary
30
+
31
+ The default validation mode is `safe`. It is the mode intended for hostile
32
+ boundary data. Safe mode avoids user getter execution, treats prototype-backed
33
+ data as untrusted, handles `__proto__` and `constructor` keys with
34
+ null-prototype lookups, checks strict-object symbol and non-enumerable extras,
35
+ and returns explicit `Result` values for expected failures.
36
+
37
+ `unsafe` and `unchecked` are performance escape hatches for trusted,
38
+ already-normalized data. They may execute getters, may accept prototype-backed
39
+ values, and may relax strict-object extra-key guarantees. Do not use these modes
40
+ on public input boundaries unless a separate normalization step has already
41
+ converted the input into plain owned data.
42
+
43
+ `compile()` uses `new Function` by design. If a deployment forbids dynamic code
44
+ generation through Content Security Policy, use normal guards or
45
+ `emitAotModule()` instead.
46
+
47
+ ## Release Integrity
48
+
49
+ The package is expected to have zero runtime, peer, optional, and bundled
50
+ dependencies. Release checks verify package contents, public API drift, docs,
51
+ tests, consumer install smoke, benchmarks, and dist policy before publishing.
52
+ Normal releases should go through GitHub Releases so npm provenance is attached.
package/docs/api.md CHANGED
@@ -148,6 +148,12 @@ const Count = t.pipe(t.coerce.number(), t.number.int().gte(0));
148
148
  const result = Count.decode("42");
149
149
 
150
150
  const Name = t.default(t.string.min(1), "anonymous");
151
+ const NormalizedName = t.string
152
+ .trim()
153
+ .pipe(t.string.min(1))
154
+ .transform((value) => value.toLowerCase())
155
+ .default("anonymous")
156
+ .catch("anonymous");
151
157
  const NumberText = t.codec(
152
158
  t.string.regex(/^\d+$/u, "digits"),
153
159
  t.number.int().nonnegative(),
@@ -163,12 +169,11 @@ Decoders are for output-producing operations. They return `Result` from
163
169
  not be the same runtime value as the input.
164
170
 
165
171
  - `t.transform(source, mapper)` decodes `source`, then maps the decoded value.
166
- - `t.pipe(source, next)` feeds a successful decoded value into the next guard or
167
- decoder.
172
+ - `t.pipe(source, next)` feeds a successful decoded value into the next guard or decoder.
168
173
  - `t.default(source, value)` returns a fallback output for `undefined` input.
169
174
  - `t.prefault(source, value)` feeds a fallback input through the source.
170
- - `t.codec(input, output, mapping)` validates both sides of a bidirectional
171
- decode/encode pair.
175
+ - `t.catch(source, value)` returns a fallback output after a failed decode.
176
+ - `t.codec(input, output, mapping)` validates both sides of a bidirectional decode/encode pair.
172
177
  - `t.coerce.string`, `t.coerce.number`, and `t.coerce.boolean` provide explicit
173
178
  primitive coercion.
174
179
  - `t.string.trim()`, `t.string.toLowerCase()`, and `t.string.toUpperCase()`
@@ -320,9 +325,10 @@ const fastParser = toTrpcParser(FastUser);
320
325
  const fastValidatorCompiler = toFastifyValidatorCompiler(FastUser);
321
326
  ```
322
327
 
323
- Use the default compiled mode at public input boundaries. For trusted,
324
- already-normalized internal data, the faster modes can be wired through adapters
325
- the same way.
328
+ Use the default compiled mode at public input boundaries. It keeps the safe
329
+ descriptor-read contract even when an adapter hides the direct `is()` call. For
330
+ trusted, already-normalized internal data, the faster modes can be wired through
331
+ adapters the same way.
326
332
 
327
333
  ```ts
328
334
  const UnsafeUser = compile(User, { mode: "unsafe" });
@@ -58,92 +58,92 @@
58
58
  <circle cx="84" cy="690" r="220" fill="#38bdf8" opacity="0.07"/>
59
59
 
60
60
  <text id="headline" x="44" y="58" class="title">TypeSea benchmark comparison</text>
61
- <text x="46" y="88" class="subtitle">Local run on 2026-07-04 KST, strict-object contract, ops/sec. Higher is better.</text>
61
+ <text x="46" y="88" class="subtitle">Local run on 2026-07-05 KST, strict-object contract, ops/sec. Higher is better.</text>
62
62
 
63
63
  <g filter="url(#shadow)">
64
64
  <rect x="44" y="118" width="322" height="108" rx="18" fill="url(#panel)" stroke="#1f2937"/>
65
65
  <text x="66" y="148" class="metric-label">Unchecked valid hot path</text>
66
- <text x="66" y="188" class="metric-value">42.58M</text>
67
- <text x="66" y="210" class="metric-sub">31.7x Zod, 10.0x Ajv</text>
66
+ <text x="66" y="188" class="metric-value">42.62M</text>
67
+ <text x="66" y="210" class="metric-sub">30.4x Zod, 10.1x Ajv</text>
68
68
 
69
69
  <rect x="399" y="118" width="322" height="108" rx="18" fill="url(#panel)" stroke="#1f2937"/>
70
70
  <text x="421" y="148" class="metric-label">Safe invalid fast-fail</text>
71
- <text x="421" y="188" class="metric-value">42.08M</text>
72
- <text x="421" y="210" class="metric-sub">1.51x Ajv, 499x Zod</text>
71
+ <text x="421" y="188" class="metric-value">43.09M</text>
72
+ <text x="421" y="210" class="metric-sub">1.41x Ajv, 509x Zod</text>
73
73
 
74
74
  <rect x="754" y="118" width="322" height="108" rx="18" fill="url(#panel)" stroke="#1f2937"/>
75
75
  <text x="776" y="148" class="metric-label">Safe valid path</text>
76
- <text x="776" y="188" class="metric-value">4.30M</text>
77
- <text x="776" y="210" class="metric-sub">Ajv-class while staying safe</text>
76
+ <text x="776" y="188" class="metric-value">5.11M</text>
77
+ <text x="776" y="210" class="metric-sub">1.21x Ajv while staying safe</text>
78
78
  </g>
79
79
 
80
80
  <g filter="url(#shadow)">
81
81
  <rect x="44" y="260" width="1032" height="136" rx="18" fill="url(#panel)" stroke="#1f2937"/>
82
82
  <text x="66" y="291" class="card-title">Valid object path</text>
83
- <text x="230" y="291" class="card-note">linear scale to 42.58M</text>
83
+ <text x="230" y="291" class="card-note">linear scale to 42.62M</text>
84
84
  <line x1="270" y1="318" x2="1010" y2="318" class="axis"/>
85
85
 
86
86
  <text x="66" y="326" class="bar-label">TypeSea unchecked</text>
87
87
  <rect x="270" y="312" width="740" height="15" rx="7.5" fill="url(#unchecked)"/>
88
- <text x="1024" y="326" class="bar-value">42.58M</text>
88
+ <text x="1024" y="326" class="bar-value">42.62M</text>
89
89
 
90
90
  <text x="66" y="350" class="bar-label">TypeSea unsafe</text>
91
- <rect x="270" y="336" width="631" height="15" rx="7.5" fill="url(#unsafe)"/>
92
- <text x="915" y="350" class="bar-value">36.30M</text>
91
+ <rect x="270" y="336" width="639" height="15" rx="7.5" fill="url(#unsafe)"/>
92
+ <text x="923" y="350" class="bar-value">36.78M</text>
93
93
 
94
94
  <text x="66" y="374" class="bar-label">TypeSea safe / Ajv / Zod</text>
95
- <rect x="270" y="360" width="75" height="15" rx="7.5" fill="url(#safe)"/>
96
- <rect x="354" y="360" width="74" height="15" rx="7.5" fill="url(#ajv)"/>
97
- <rect x="437" y="360" width="24" height="15" rx="7.5" fill="url(#valibot)"/>
98
- <rect x="470" y="360" width="23" height="15" rx="7.5" fill="url(#zod)"/>
99
- <text x="506" y="374" class="bar-value">safe 4.30M, Ajv 4.28M, Valibot 1.41M, Zod 1.34M</text>
95
+ <rect x="270" y="360" width="89" height="15" rx="7.5" fill="url(#safe)"/>
96
+ <rect x="368" y="360" width="74" height="15" rx="7.5" fill="url(#ajv)"/>
97
+ <rect x="451" y="360" width="24" height="15" rx="7.5" fill="url(#valibot)"/>
98
+ <rect x="484" y="360" width="24" height="15" rx="7.5" fill="url(#zod)"/>
99
+ <text x="522" y="374" class="bar-value">safe 5.11M, Ajv 4.24M, Valibot 1.40M, Zod 1.40M</text>
100
100
  </g>
101
101
 
102
102
  <g filter="url(#shadow)">
103
103
  <rect x="44" y="418" width="1032" height="150" rx="18" fill="url(#panel)" stroke="#1f2937"/>
104
104
  <text x="66" y="449" class="card-title">Invalid object fast-fail</text>
105
- <text x="268" y="449" class="card-note">linear scale to 50.48M</text>
105
+ <text x="268" y="449" class="card-note">linear scale to 50.90M</text>
106
106
  <line x1="270" y1="476" x2="1010" y2="476" class="axis"/>
107
107
 
108
108
  <text x="66" y="482" class="bar-label">TypeSea unchecked</text>
109
109
  <rect x="270" y="468" width="740" height="15" rx="7.5" fill="url(#unchecked)"/>
110
- <text x="1024" y="482" class="bar-value">50.48M</text>
110
+ <text x="1024" y="482" class="bar-value">50.90M</text>
111
111
 
112
112
  <text x="66" y="506" class="bar-label">TypeSea unsafe</text>
113
- <rect x="270" y="492" width="728" height="15" rx="7.5" fill="url(#unsafe)"/>
114
- <text x="1024" y="506" class="bar-value">49.65M</text>
113
+ <rect x="270" y="492" width="738" height="15" rx="7.5" fill="url(#unsafe)"/>
114
+ <text x="1024" y="506" class="bar-value">50.74M</text>
115
115
 
116
116
  <text x="66" y="530" class="bar-label">TypeSea safe</text>
117
- <rect x="270" y="516" width="617" height="15" rx="7.5" fill="url(#safe)"/>
118
- <text x="902" y="530" class="bar-value">42.08M</text>
117
+ <rect x="270" y="516" width="627" height="15" rx="7.5" fill="url(#safe)"/>
118
+ <text x="912" y="530" class="bar-value">43.09M</text>
119
119
 
120
120
  <text x="66" y="554" class="bar-label">Ajv / Valibot / Zod</text>
121
- <rect x="270" y="540" width="408" height="15" rx="7.5" fill="url(#ajv)"/>
122
- <rect x="690" y="540" width="13" height="15" rx="7.5" fill="url(#valibot)"/>
123
- <rect x="714" y="540" width="3" height="15" rx="1.5" fill="url(#zod)"/>
124
- <text x="734" y="554" class="bar-value">Ajv 27.82M, Valibot 0.88M, Zod 0.08M</text>
121
+ <rect x="270" y="540" width="444" height="15" rx="7.5" fill="url(#ajv)"/>
122
+ <rect x="726" y="540" width="13" height="15" rx="7.5" fill="url(#valibot)"/>
123
+ <rect x="750" y="540" width="3" height="15" rx="1.5" fill="url(#zod)"/>
124
+ <text x="770" y="554" class="bar-value">Ajv 30.54M, Valibot 0.87M, Zod 0.08M</text>
125
125
  </g>
126
126
 
127
127
  <g filter="url(#shadow)">
128
128
  <rect x="44" y="576" width="1032" height="136" rx="18" fill="url(#panel)" stroke="#1f2937"/>
129
129
  <text x="66" y="607" class="card-title">Invalid diagnostic path</text>
130
- <text x="270" y="607" class="card-note">linear scale to 28.71M; Ajv is a boolean baseline</text>
130
+ <text x="270" y="607" class="card-note">linear scale to 29.95M; Ajv is a boolean baseline</text>
131
131
  <line x1="270" y1="634" x2="1010" y2="634" class="axis"/>
132
132
 
133
133
  <text x="66" y="642" class="bar-label">Ajv compiled</text>
134
134
  <rect x="270" y="628" width="740" height="15" rx="7.5" fill="url(#ajv)"/>
135
- <text x="1024" y="642" class="bar-value">28.71M</text>
135
+ <text x="1024" y="642" class="bar-value">29.95M</text>
136
136
 
137
137
  <text x="66" y="666" class="bar-label">TypeSea modes</text>
138
- <rect x="270" y="652" width="95" height="15" rx="7.5" fill="url(#unchecked)"/>
138
+ <rect x="270" y="652" width="87" height="15" rx="7.5" fill="url(#unchecked)"/>
139
139
  <rect x="374" y="652" width="79" height="15" rx="7.5" fill="url(#unsafe)"/>
140
- <rect x="462" y="652" width="54" height="15" rx="7.5" fill="url(#safe)"/>
141
- <text x="532" y="666" class="bar-value">unchecked 3.67M, unsafe 3.08M, safe 2.09M</text>
140
+ <rect x="462" y="652" width="52" height="15" rx="7.5" fill="url(#safe)"/>
141
+ <text x="532" y="666" class="bar-value">unchecked 3.51M, unsafe 3.19M, safe 2.11M</text>
142
142
 
143
143
  <text x="66" y="690" class="bar-label">Valibot / Zod</text>
144
- <rect x="270" y="676" width="23" height="15" rx="7.5" fill="url(#valibot)"/>
144
+ <rect x="270" y="676" width="20" height="15" rx="7.5" fill="url(#valibot)"/>
145
145
  <rect x="302" y="676" width="3" height="15" rx="1.5" fill="url(#zod)"/>
146
- <text x="322" y="690" class="bar-value">Valibot 0.89M, Zod 0.08M</text>
146
+ <text x="322" y="690" class="bar-value">Valibot 0.79M, Zod 0.09M</text>
147
147
  </g>
148
148
 
149
149
  <g transform="translate(44 730)">
@@ -185,15 +185,15 @@ Zod, Valibot, and Ajv are dev dependencies for measurement only. They are not
185
185
  imported by `src`, and package policy rejects runtime, peer, optional, or
186
186
  bundled dependency fields before release.
187
187
 
188
- Last local benchmark on 2026-07-04 KST reported these ecosystem paths over the
188
+ Last local benchmark on 2026-07-05 KST reported these ecosystem paths over the
189
189
  JSON-compatible strict-object benchmark:
190
190
 
191
191
  | Case | TypeSea runtime plan | TypeSea compiled safe | TypeSea compiled unsafe | TypeSea compiled unchecked | Ajv compiled |
192
192
  | --- | ---: | ---: | ---: | ---: | ---: |
193
- | Valid `is()` | 513,701 hz | 4,297,306 hz | 36,297,653 hz | 42,581,174 hz | 4,275,389 hz |
194
- | Valid `check()` | 503,232 hz | 3,903,929 hz | 35,568,425 hz | 40,084,605 hz | 4,278,587 hz |
195
- | Invalid `is()` | 3,636,369 hz | 42,080,241 hz | 49,654,076 hz | 50,482,732 hz | 27,820,643 hz |
196
- | Invalid `check()` | 420,446 hz | 2,086,129 hz | 3,077,367 hz | 3,673,508 hz | 28,713,035 hz |
193
+ | Valid `is()` | 478,576 hz | 5,109,602 hz | 36,777,097 hz | 42,620,570 hz | 4,238,036 hz |
194
+ | Valid `check()` | 424,989 hz | 4,642,948 hz | 37,184,199 hz | 42,487,325 hz | 4,338,063 hz |
195
+ | Invalid `is()` | 3,325,603 hz | 43,094,061 hz | 50,738,235 hz | 50,898,012 hz | 30,535,761 hz |
196
+ | Invalid `check()` | 405,590 hz | 2,107,460 hz | 3,186,702 hz | 3,509,673 hz | 29,951,403 hz |
197
197
 
198
198
  Benchmark numbers are machine-local telemetry. They are useful for catching
199
199
  regressions, not for promising a fixed throughput floor. Unsafe and unchecked
package/docs/index.html CHANGED
@@ -604,7 +604,7 @@
604
604
  <aside class="sidebar" aria-label="Documentation navigation">
605
605
  <div class="brand">
606
606
  <h1>TypeSea</h1>
607
- <span class="version">v0.3.0</span>
607
+ <span class="version">v0.3.1</span>
608
608
  </div>
609
609
  <p class="brand-tagline">
610
610
  <span class="i18n-en">Complete docs from README.md, docs/api.md, and docs/engine-notes.md.</span>
@@ -966,14 +966,15 @@ const schema = toJsonSchema(User);</code></pre>
966
966
  </header>
967
967
  <div class="locale-en">
968
968
  <h2 id="readme-en-typesea">TypeSea</h2>
969
- <p><a href="https://github.com/Feralthedogg/TypeSea/actions/workflows/ci.yml/badge.svg">![CI</a>](https://github.com/Feralthedogg/TypeSea/actions/workflows/ci.yml) <span class="muted-link">![Socket Badge</span>](https://badge.socket.dev/npm/package/typesea/0.3.0) <span class="muted-link">![License</span>](./LICENSE) <span class="muted-link">TypeScript</span> <span class="muted-link">Dependencies</span> <span class="muted-link">Tree-shakeable</span> <span class="muted-link">Side-effect free</span> <span class="muted-link">No dependencies</span> <span class="muted-link">Module</span> <span class="muted-link">Node</span></p>
969
+ <p><a href="https://github.com/Feralthedogg/TypeSea/actions/workflows/ci.yml/badge.svg">![CI</a>](https://github.com/Feralthedogg/TypeSea/actions/workflows/ci.yml) <span class="muted-link">![Socket Badge</span>](https://socket.dev/npm/package/typesea) <span class="muted-link">![License</span>](./LICENSE) <span class="muted-link">TypeScript</span> <span class="muted-link">Dependencies</span> <span class="muted-link">Tree-shakeable</span> <span class="muted-link">Side-effect free</span> <span class="muted-link">No dependencies</span> <span class="muted-link">Module</span> <span class="muted-link">Node</span></p>
970
970
  <p><strong>TypeSea</strong> is a <strong>zero-runtime-dependency TypeScript runtime narrowing library</strong> built around <strong>immutable guards</strong>, optimized <strong>Sea-of-Nodes validation plans</strong>, runtime compilation, and AOT source generation.</p>
971
971
  <h3 id="readme-en-benchmark-headline">Benchmark Headline</h3>
972
- <p>Last local benchmark on 2026-07-04 KST: <code>npm run bench -- bench/ecosystem.bench.ts --run</code>, strict-object contract, operations per second on one machine.</p>
972
+ <p>Last local benchmark on 2026-07-05 KST: <code>npm run bench -- bench/ecosystem.bench.ts --run</code>, strict-object contract, operations per second on one machine.</p>
973
973
  <p><img class="benchmark-image" src="assets/benchmark-headline.svg" alt="TypeSea benchmark comparison"></p>
974
974
  <p>TypeSea safe compiled validators are already in Ajv's boolean hot-path class while keeping descriptor-based hostile-input semantics. Unsafe and unchecked FastMode are the bragging-rights path for trusted normalized data: direct field loads, allocation-light strict-key loops, and V8-friendly monomorphic codegen.</p>
975
975
  <blockquote><p>Goal: not &quot;probably valid&quot;, but <strong>provably parity-tested validation</strong> that never executes user code, never throws on expected failures, and never leaks mutable state across a public boundary.</p></blockquote>
976
976
  <aside class="admonition important"><strong class="admonition-title">IMPORTANT</strong><p>TypeSea is designed for <strong>hostile boundary data</strong>: property reads go through descriptors so <strong>user getters never execute</strong>, <code>__proto__</code>/<code>constructor</code> keys are handled with null-prototype lookups, user regexes are cloned and <code>lastIndex</code>-reset, and cyclic inputs validate finitely. Expected failures return frozen <code>Result</code> values — <code>any</code>, <code>try</code>, and <code>catch</code> are banned from the entire codebase and enforced by policy gates.</p></aside>
977
+ <aside class="admonition warning"><strong class="admonition-title">WARNING</strong><p><code>unsafe</code> and <code>unchecked</code> are <strong>not public-boundary modes</strong>. They are for trusted, already-normalized data where the caller accepts getter execution, prototype-backed values, and weaker strict-extra-key guarantees. Use the default safe mode for external input.</p></aside>
977
978
  <hr>
978
979
  <h3 id="readme-en-why">Why</h3>
979
980
  <p>Many validation libraries fall short when you care about:</p>
@@ -1059,35 +1060,35 @@ failed check() -&gt; schema-aware diagnostic collector</code></pre>
1059
1060
  <aside class="admonition important"><strong class="admonition-title">IMPORTANT</strong><p>Generated validators keep <strong>user-controlled values out of source text</strong>: literals, regexps, object keys, keysets, and dynamic schema fallbacks live in <strong>side tables</strong> referenced by numeric index. Hostile property names cannot escape into generated code — this is pinned by dedicated injection-audit tests.</p></aside>
1060
1061
  <hr>
1061
1062
  <h3 id="readme-en-performance-snapshot">Performance Snapshot</h3>
1062
- <p>Last local benchmark on 2026-07-04 KST, using <code>npm run bench -- bench/ecosystem.bench.ts --run</code> on the benchmark strict-object contract. These are operations per second on one machine, not release guarantees.</p>
1063
- <div class="table-wrap"><table><thead><tr><th>Valid object path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>513,701</td></tr>
1064
- <tr><td>TypeSea compiled safe <code>is()</code></td><td>4,297,306</td></tr>
1065
- <tr><td>TypeSea compiled unsafe <code>is()</code></td><td>36,297,653</td></tr>
1066
- <tr><td>TypeSea compiled unchecked <code>is()</code></td><td>42,581,174</td></tr>
1067
- <tr><td>Zod <code>safeParse</code></td><td>1,343,756</td></tr>
1068
- <tr><td>Valibot <code>safeParse</code></td><td>1,406,528</td></tr>
1069
- <tr><td>Ajv compiled</td><td>4,275,389</td></tr></tbody></table></div>
1070
- <div class="table-wrap"><table><thead><tr><th>Valid diagnostic path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>503,232</td></tr>
1071
- <tr><td>TypeSea compiled safe <code>check()</code></td><td>3,903,929</td></tr>
1072
- <tr><td>TypeSea compiled unsafe <code>check()</code></td><td>35,568,425</td></tr>
1073
- <tr><td>TypeSea compiled unchecked <code>check()</code></td><td>40,084,605</td></tr>
1074
- <tr><td>Zod <code>safeParse</code></td><td>1,355,014</td></tr>
1075
- <tr><td>Valibot <code>safeParse</code></td><td>1,378,266</td></tr>
1076
- <tr><td>Ajv compiled</td><td>4,278,587</td></tr></tbody></table></div>
1077
- <div class="table-wrap"><table><thead><tr><th>Invalid object path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>3,636,369</td></tr>
1078
- <tr><td>TypeSea compiled safe <code>is()</code></td><td>42,080,241</td></tr>
1079
- <tr><td>TypeSea compiled unsafe <code>is()</code></td><td>49,654,076</td></tr>
1080
- <tr><td>TypeSea compiled unchecked <code>is()</code></td><td>50,482,732</td></tr>
1081
- <tr><td>Zod <code>safeParse</code></td><td>84,272</td></tr>
1082
- <tr><td>Valibot <code>safeParse</code></td><td>878,521</td></tr>
1083
- <tr><td>Ajv compiled</td><td>27,820,643</td></tr></tbody></table></div>
1084
- <div class="table-wrap"><table><thead><tr><th>Invalid diagnostic path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>420,446</td></tr>
1085
- <tr><td>TypeSea compiled safe <code>check()</code></td><td>2,086,129</td></tr>
1086
- <tr><td>TypeSea compiled unsafe <code>check()</code></td><td>3,077,367</td></tr>
1087
- <tr><td>TypeSea compiled unchecked <code>check()</code></td><td>3,673,508</td></tr>
1088
- <tr><td>Zod <code>safeParse</code></td><td>79,613</td></tr>
1089
- <tr><td>Valibot <code>safeParse</code></td><td>887,991</td></tr>
1090
- <tr><td>Ajv compiled</td><td>28,713,035</td></tr></tbody></table></div>
1063
+ <p>Last local benchmark on 2026-07-05 KST, using <code>npm run bench -- bench/ecosystem.bench.ts --run</code> on the benchmark strict-object contract. These are operations per second on one machine, not release guarantees.</p>
1064
+ <div class="table-wrap"><table><thead><tr><th>Valid object path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>478,576</td></tr>
1065
+ <tr><td>TypeSea compiled safe <code>is()</code></td><td>5,109,602</td></tr>
1066
+ <tr><td>TypeSea compiled unsafe <code>is()</code></td><td>36,777,097</td></tr>
1067
+ <tr><td>TypeSea compiled unchecked <code>is()</code></td><td>42,620,570</td></tr>
1068
+ <tr><td>Zod <code>safeParse</code></td><td>1,400,045</td></tr>
1069
+ <tr><td>Valibot <code>safeParse</code></td><td>1,400,599</td></tr>
1070
+ <tr><td>Ajv compiled</td><td>4,238,036</td></tr></tbody></table></div>
1071
+ <div class="table-wrap"><table><thead><tr><th>Valid diagnostic path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>424,989</td></tr>
1072
+ <tr><td>TypeSea compiled safe <code>check()</code></td><td>4,642,948</td></tr>
1073
+ <tr><td>TypeSea compiled unsafe <code>check()</code></td><td>37,184,199</td></tr>
1074
+ <tr><td>TypeSea compiled unchecked <code>check()</code></td><td>42,487,325</td></tr>
1075
+ <tr><td>Zod <code>safeParse</code></td><td>1,278,859</td></tr>
1076
+ <tr><td>Valibot <code>safeParse</code></td><td>1,391,040</td></tr>
1077
+ <tr><td>Ajv compiled</td><td>4,338,063</td></tr></tbody></table></div>
1078
+ <div class="table-wrap"><table><thead><tr><th>Invalid object path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>3,325,603</td></tr>
1079
+ <tr><td>TypeSea compiled safe <code>is()</code></td><td>43,094,061</td></tr>
1080
+ <tr><td>TypeSea compiled unsafe <code>is()</code></td><td>50,738,235</td></tr>
1081
+ <tr><td>TypeSea compiled unchecked <code>is()</code></td><td>50,898,012</td></tr>
1082
+ <tr><td>Zod <code>safeParse</code></td><td>84,647</td></tr>
1083
+ <tr><td>Valibot <code>safeParse</code></td><td>866,013</td></tr>
1084
+ <tr><td>Ajv compiled</td><td>30,535,761</td></tr></tbody></table></div>
1085
+ <div class="table-wrap"><table><thead><tr><th>Invalid diagnostic path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>405,590</td></tr>
1086
+ <tr><td>TypeSea compiled safe <code>check()</code></td><td>2,107,460</td></tr>
1087
+ <tr><td>TypeSea compiled unsafe <code>check()</code></td><td>3,186,702</td></tr>
1088
+ <tr><td>TypeSea compiled unchecked <code>check()</code></td><td>3,509,673</td></tr>
1089
+ <tr><td>Zod <code>safeParse</code></td><td>85,355</td></tr>
1090
+ <tr><td>Valibot <code>safeParse</code></td><td>788,870</td></tr>
1091
+ <tr><td>Ajv compiled</td><td>29,951,403</td></tr></tbody></table></div>
1091
1092
  <p>The safe compiled path stays close to Ajv while retaining TypeSea hostile-input semantics: descriptor-based property reads, symbol/non-enumerable strict-key rejection, presence semantics, immutable diagnostics, and TypeScript guard inference. Unsafe and unchecked compiled modes are faster because they deliberately give up parts of that hostile-input contract.</p>
1092
1093
  <hr>
1093
1094
  <h3 id="readme-en-api-reference">API Reference</h3>
@@ -1170,12 +1171,20 @@ const Node: Guard&lt;ListNode&gt; = t.lazy((): Guard&lt;ListNode&gt; =&gt;
1170
1171
  npm run check:consumer # tarball install + runtime/type smoke in a temp project
1171
1172
  npm run bench -- --run # benchmark smoke
1172
1173
  npm run pack:dry # package contents dry run
1173
- npm run release:check # the full pre-publish gate (everything above)</code></pre>
1174
+ npm run release:check # the full pre-publish gate (everything above)
1175
+ npm run release:publish # npm publish with provenance and ignored lifecycle scripts</code></pre>
1174
1176
  <p><code>npm run release:check</code> runs the same gate expected before publishing: typecheck, lint, tests, build, docs smoke, dist policy, public API snapshot, package contents, consumer install, benchmark smoke, and pack dry run. CI executes it on Node 20.19, 22, and 24; releases publish with npm provenance.</p>
1177
+ <p>Release path:</p>
1178
+ <ol><li>Push a <code>vX.Y.Z</code> tag or run the GitHub <code>Release</code> workflow with that tag.</li><li>The release workflow verifies that the tag matches <code>package.json</code>.</li><li>Publishing happens from the GitHub <code>Publish</code> workflow through <code>npm run release:publish</code>, which expands to <code>npm publish --provenance --access public --ignore-scripts</code>.</li></ol>
1179
+ <p>Local publishing with <code>NPM_TOKEN</code> is reserved for manual recovery releases. It must still run <code>npm run release:check</code> first, and it cannot attach GitHub OIDC provenance.</p>
1175
1180
  <aside class="admonition note"><strong class="admonition-title">NOTE</strong><p>Benchmark comparison packages (Zod, Valibot, Ajv) are dev dependencies only — package policy rejects them from every runtime dependency field. The benchmark suite reports both boolean-path and diagnostic-path (<code>check()</code> vs <code>safeParse</code>) comparisons, so numbers stay apples-to-apples.</p></aside>
1176
1181
  <hr>
1177
1182
  <h3 id="readme-en-documentation">Documentation</h3>
1178
- <ul><li><a href="#overview">Documentation site</a></li><li><a href="#api-reference">API reference</a></li><li><a href="#engine-notes">Engine notes</a></li></ul>
1183
+ <ul><li><a href="#overview">Documentation site</a></li><li><a href="#api-reference">API reference</a></li><li><a href="#engine-notes">Engine notes</a></li><li><a href="https://github.com/Feralthedogg/TypeSea/blob/main/SECURITY.md">Security policy</a></li></ul>
1184
+ <hr>
1185
+ <h3 id="readme-en-migration-notes">Migration Notes</h3>
1186
+ <h4 id="readme-en-0-3-0-to-0-3-1">0.3.0 to 0.3.1</h4>
1187
+ <p>No application code changes are required. <code>0.3.1</code> is a release-hardening patch: it tightens manual release tag handling, documents npm provenance expectations, adds a security policy, and verifies that npm exposes the published version after the GitHub publish workflow completes.</p>
1179
1188
  <hr>
1180
1189
  <h3 id="readme-en-license">License</h3>
1181
1190
  <p>MIT License. See <a href="https://github.com/Feralthedogg/TypeSea/blob/main/LICENSE">LICENSE</a>.</p>
@@ -1184,11 +1193,12 @@ npm run release:check # the full pre-publish gate (everything above)</code></p
1184
1193
  <h2 id="readme-ko-typesea">TypeSea</h2>
1185
1194
  <p><strong>TypeSea</strong>는 런타임 의존성 없이 TypeScript 값을 검증하고 타입을 좁히는 라이브러리입니다. 불변 스키마, Sea-of-Nodes에서 영향을 받은 검증 IR, 런타임 컴파일, AOT 소스 생성을 한 흐름으로 묶는 것을 목표로 합니다.</p>
1186
1195
  <h3 id="readme-ko-벤치마크-요약">벤치마크 요약</h3>
1187
- <p>마지막 로컬 벤치마크는 2026-07-04 KST에 실행했습니다. 명령은 <code>npm run bench -- bench/ecosystem.bench.ts --run</code>이며, strict object 계약을 대상으로 한 단일 머신의 초당 실행 횟수입니다. 아래 수치는 회귀를 잡기 위한 로컬 측정값이지, 릴리스 성능 보증값은 아닙니다.</p>
1196
+ <p>마지막 로컬 벤치마크는 2026-07-05 KST에 실행했습니다. 명령은 <code>npm run bench -- bench/ecosystem.bench.ts --run</code>이며, strict object 계약을 대상으로 한 단일 머신의 초당 실행 횟수입니다. 아래 수치는 회귀를 잡기 위한 로컬 측정값이지, 릴리스 성능 보증값은 아닙니다.</p>
1188
1197
  <p><img class="benchmark-image" src="assets/benchmark-headline.svg" alt="TypeSea benchmark comparison"></p>
1189
1198
  <p>TypeSea의 안전 모드 컴파일 검증기는 getter 실행 방지와 strict extra key 검사 같은 적대적 입력 방어를 유지하면서도 Ajv의 boolean hot path에 가까운 성능을 냅니다. <code>unsafe</code>와 <code>unchecked</code> FastMode는 호출자가 이미 입력을 정규화했고 객체 그래프를 신뢰할 수 있을 때 쓰는 성능 우선 경로입니다. 이 모드에서는 직접 필드 로드, 할당을 줄인 strict-key loop, V8이 inline cache를 붙이기 쉬운 코드 형태를 사용합니다.</p>
1190
1199
  <blockquote><p>목표는 &quot;대충 유효해 보이면 통과&quot;가 아닙니다. TypeSea의 목표는 <strong>런타임 실행, 컴파일 실행, AOT 실행이 같은 판정을 내린다는 사실을 테스트로 고정하는 검증기</strong>입니다. 사용자 코드를 실행하지 않고, 예상 가능한 실패에서 예외를 던지지 않으며, 공개 API 경계 밖으로 변경 가능한 내부 상태를 내보내지 않는 것을 기본 원칙으로 둡니다.</p></blockquote>
1191
1200
  <aside class="admonition important"><strong class="admonition-title">IMPORTANT</strong><p>TypeSea는 <strong>적대적인 경계 입력</strong>을 전제로 설계했습니다. 속성 읽기는 descriptor를 통하므로 <strong>사용자 getter를 실행하지 않습니다</strong>. <code>__proto__</code>와 <code>constructor</code> key는 null-prototype lookup으로 처리하고, 사용자 regexp는 복제한 뒤 <code>lastIndex</code>를 reset하며, 순환 입력도 유한하게 검증합니다. 예상 가능한 실패는 동결된 <code>Result</code>로 반환합니다. 불명확한 타입 탈출과 암묵적 예외 흐름에 기대지 않도록 코드베이스 전체에 정책 게이트를 둡니다.</p></aside>
1201
+ <aside class="admonition warning"><strong class="admonition-title">WARNING</strong><p><code>unsafe</code>와 <code>unchecked</code>는 <strong>public boundary용 모드가 아닙니다</strong>. 이미 신뢰 가능한 plain data로 정규화된 입력에서만 사용하세요. 이 모드에서는 getter 실행, prototype-backed value 수용, 더 약한 strict extra-key 보장을 호출자가 받아들이는 것입니다. 외부 입력에는 기본 safe mode를 쓰는 것이 TypeSea의 보안 계약입니다.</p></aside>
1192
1202
  <hr>
1193
1203
  <h3 id="readme-ko-왜-만들었나">왜 만들었나</h3>
1194
1204
  <p>검증 라이브러리를 실제 경계 입력에 쓰다 보면 다음 조건을 동시에 만족시키기 어렵습니다.</p>
@@ -1263,35 +1273,35 @@ failed check() -&gt; schema-aware diagnostic collector</code></pre>
1263
1273
  <aside class="admonition important"><strong class="admonition-title">IMPORTANT</strong><p>generated validator는 <strong>사용자가 제어하는 값을 소스 문자열에 넣지 않습니다</strong>. literal, regexp, object key, keyset, dynamic schema fallback은 numeric index로 참조되는 <strong>side table</strong>에 둡니다. 적대적인 property name이 generated code 밖으로 탈출할 수 없으며, dedicated injection-audit test가 이 속성을 고정합니다.</p></aside>
1264
1274
  <hr>
1265
1275
  <h3 id="readme-ko-성능-스냅샷">성능 스냅샷</h3>
1266
- <p>마지막 로컬 벤치마크는 2026-07-04 KST에 실행했습니다. <code>npm run bench -- bench/ecosystem.bench.ts --run</code>을 사용했고, benchmark strict-object 계약을 대상으로 했습니다. 아래 값은 단일 머신의 초당 실행 횟수이며 릴리스 성능 보증값은 아닙니다.</p>
1267
- <div class="table-wrap"><table><thead><tr><th>유효한 객체: boolean 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>513,701</td></tr>
1268
- <tr><td>TypeSea compiled safe <code>is()</code></td><td>4,297,306</td></tr>
1269
- <tr><td>TypeSea compiled unsafe <code>is()</code></td><td>36,297,653</td></tr>
1270
- <tr><td>TypeSea compiled unchecked <code>is()</code></td><td>42,581,174</td></tr>
1271
- <tr><td>Zod <code>safeParse</code></td><td>1,343,756</td></tr>
1272
- <tr><td>Valibot <code>safeParse</code></td><td>1,406,528</td></tr>
1273
- <tr><td>Ajv compiled</td><td>4,275,389</td></tr></tbody></table></div>
1274
- <div class="table-wrap"><table><thead><tr><th>유효한 객체: 진단 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>503,232</td></tr>
1275
- <tr><td>TypeSea compiled safe <code>check()</code></td><td>3,903,929</td></tr>
1276
- <tr><td>TypeSea compiled unsafe <code>check()</code></td><td>35,568,425</td></tr>
1277
- <tr><td>TypeSea compiled unchecked <code>check()</code></td><td>40,084,605</td></tr>
1278
- <tr><td>Zod <code>safeParse</code></td><td>1,355,014</td></tr>
1279
- <tr><td>Valibot <code>safeParse</code></td><td>1,378,266</td></tr>
1280
- <tr><td>Ajv compiled</td><td>4,278,587</td></tr></tbody></table></div>
1281
- <div class="table-wrap"><table><thead><tr><th>잘못된 객체: boolean 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>3,636,369</td></tr>
1282
- <tr><td>TypeSea compiled safe <code>is()</code></td><td>42,080,241</td></tr>
1283
- <tr><td>TypeSea compiled unsafe <code>is()</code></td><td>49,654,076</td></tr>
1284
- <tr><td>TypeSea compiled unchecked <code>is()</code></td><td>50,482,732</td></tr>
1285
- <tr><td>Zod <code>safeParse</code></td><td>84,272</td></tr>
1286
- <tr><td>Valibot <code>safeParse</code></td><td>878,521</td></tr>
1287
- <tr><td>Ajv compiled</td><td>27,820,643</td></tr></tbody></table></div>
1288
- <div class="table-wrap"><table><thead><tr><th>잘못된 객체: 진단 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>420,446</td></tr>
1289
- <tr><td>TypeSea compiled safe <code>check()</code></td><td>2,086,129</td></tr>
1290
- <tr><td>TypeSea compiled unsafe <code>check()</code></td><td>3,077,367</td></tr>
1291
- <tr><td>TypeSea compiled unchecked <code>check()</code></td><td>3,673,508</td></tr>
1292
- <tr><td>Zod <code>safeParse</code></td><td>79,613</td></tr>
1293
- <tr><td>Valibot <code>safeParse</code></td><td>887,991</td></tr>
1294
- <tr><td>Ajv compiled</td><td>28,713,035</td></tr></tbody></table></div>
1276
+ <p>마지막 로컬 벤치마크는 2026-07-05 KST에 실행했습니다. <code>npm run bench -- bench/ecosystem.bench.ts --run</code>을 사용했고, benchmark strict-object 계약을 대상으로 했습니다. 아래 값은 단일 머신의 초당 실행 횟수이며 릴리스 성능 보증값은 아닙니다.</p>
1277
+ <div class="table-wrap"><table><thead><tr><th>유효한 객체: boolean 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>478,576</td></tr>
1278
+ <tr><td>TypeSea compiled safe <code>is()</code></td><td>5,109,602</td></tr>
1279
+ <tr><td>TypeSea compiled unsafe <code>is()</code></td><td>36,777,097</td></tr>
1280
+ <tr><td>TypeSea compiled unchecked <code>is()</code></td><td>42,620,570</td></tr>
1281
+ <tr><td>Zod <code>safeParse</code></td><td>1,400,045</td></tr>
1282
+ <tr><td>Valibot <code>safeParse</code></td><td>1,400,599</td></tr>
1283
+ <tr><td>Ajv compiled</td><td>4,238,036</td></tr></tbody></table></div>
1284
+ <div class="table-wrap"><table><thead><tr><th>유효한 객체: 진단 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>424,989</td></tr>
1285
+ <tr><td>TypeSea compiled safe <code>check()</code></td><td>4,642,948</td></tr>
1286
+ <tr><td>TypeSea compiled unsafe <code>check()</code></td><td>37,184,199</td></tr>
1287
+ <tr><td>TypeSea compiled unchecked <code>check()</code></td><td>42,487,325</td></tr>
1288
+ <tr><td>Zod <code>safeParse</code></td><td>1,278,859</td></tr>
1289
+ <tr><td>Valibot <code>safeParse</code></td><td>1,391,040</td></tr>
1290
+ <tr><td>Ajv compiled</td><td>4,338,063</td></tr></tbody></table></div>
1291
+ <div class="table-wrap"><table><thead><tr><th>잘못된 객체: boolean 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>3,325,603</td></tr>
1292
+ <tr><td>TypeSea compiled safe <code>is()</code></td><td>43,094,061</td></tr>
1293
+ <tr><td>TypeSea compiled unsafe <code>is()</code></td><td>50,738,235</td></tr>
1294
+ <tr><td>TypeSea compiled unchecked <code>is()</code></td><td>50,898,012</td></tr>
1295
+ <tr><td>Zod <code>safeParse</code></td><td>84,647</td></tr>
1296
+ <tr><td>Valibot <code>safeParse</code></td><td>866,013</td></tr>
1297
+ <tr><td>Ajv compiled</td><td>30,535,761</td></tr></tbody></table></div>
1298
+ <div class="table-wrap"><table><thead><tr><th>잘못된 객체: 진단 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>405,590</td></tr>
1299
+ <tr><td>TypeSea compiled safe <code>check()</code></td><td>2,107,460</td></tr>
1300
+ <tr><td>TypeSea compiled unsafe <code>check()</code></td><td>3,186,702</td></tr>
1301
+ <tr><td>TypeSea compiled unchecked <code>check()</code></td><td>3,509,673</td></tr>
1302
+ <tr><td>Zod <code>safeParse</code></td><td>85,355</td></tr>
1303
+ <tr><td>Valibot <code>safeParse</code></td><td>788,870</td></tr>
1304
+ <tr><td>Ajv compiled</td><td>29,951,403</td></tr></tbody></table></div>
1295
1305
  <p>safe compiled path는 TypeSea의 적대적 입력 방어를 유지하면서 Ajv에 가깝게 동작합니다. descriptor 기반 property read, symbol/non-enumerable strict-key rejection, key presence semantics, immutable diagnostics, TypeScript guard inference를 유지합니다. unsafe와 unchecked compiled mode는 그 방어 계약 일부를 의도적으로 포기하기 때문에 더 빠릅니다.</p>
1296
1306
  <hr>
1297
1307
  <h3 id="readme-ko-api-레퍼런스-요약">API 레퍼런스 요약</h3>
@@ -1366,12 +1376,20 @@ const Node: Guard&lt;ListNode&gt; = t.lazy((): Guard&lt;ListNode&gt; =&gt;
1366
1376
  npm run check:consumer # tarball install + runtime/type smoke in a temp project
1367
1377
  npm run bench -- --run # benchmark smoke
1368
1378
  npm run pack:dry # package contents dry run
1369
- npm run release:check # the full pre-publish gate</code></pre>
1379
+ npm run release:check # the full pre-publish gate
1380
+ npm run release:publish # provenance를 붙이고 lifecycle script를 무시하는 npm publish</code></pre>
1370
1381
  <p><code>npm run release:check</code>는 publish 전에 기대하는 동일한 gate를 실행합니다. typecheck, lint, tests, build, docs smoke, dist policy, public API snapshot, package contents, consumer install, benchmark smoke, pack dry run을 포함합니다. CI는 Node 20.19, 22, 24에서 실행하고, release는 npm provenance와 함께 publish합니다.</p>
1382
+ <p>릴리스 경로:</p>
1383
+ <ol><li><code>vX.Y.Z</code> 태그를 push하거나 GitHub <code>Release</code> workflow를 그 태그로 실행합니다.</li><li>release workflow는 tag가 <code>package.json</code>의 version과 일치하는지 확인합니다.</li><li>publish는 GitHub <code>Publish</code> workflow에서 <code>npm run release:publish</code>로 수행합니다. 이 스크립트는 <code>npm publish --provenance --access public --ignore-scripts</code>로 확장됩니다.</li></ol>
1384
+ <p>로컬 <code>NPM_TOKEN</code> publish는 수동 복구 릴리스용입니다. 이 경우에도 먼저 <code>npm run release:check</code>를 통과해야 하며, GitHub OIDC provenance는 붙지 않습니다.</p>
1371
1385
  <aside class="admonition note"><strong class="admonition-title">NOTE</strong><p>benchmark 비교 패키지인 Zod, Valibot, Ajv는 dev dependency일 뿐입니다. package policy는 이들이 runtime dependency field에 들어가는 것을 거부합니다. benchmark suite는 boolean path와 diagnostic path(<code>check()</code> vs <code>safeParse</code>)를 모두 보고하므로 비교 기준을 맞춥니다.</p></aside>
1372
1386
  <hr>
1373
1387
  <h3 id="readme-ko-문서">문서</h3>
1374
- <ul><li><a href="#overview">문서 사이트</a></li><li><a href="#api-reference">API 레퍼런스</a></li><li><a href="#engine-notes">엔진 노트</a></li></ul>
1388
+ <ul><li><a href="#overview">문서 사이트</a></li><li><a href="#api-reference">API 레퍼런스</a></li><li><a href="#engine-notes">엔진 노트</a></li><li><a href="https://github.com/Feralthedogg/TypeSea/blob/main/SECURITY.md">보안 정책</a></li></ul>
1389
+ <hr>
1390
+ <h3 id="readme-ko-마이그레이션-노트">마이그레이션 노트</h3>
1391
+ <h4 id="readme-ko-0-3-0에서-0-3-1">0.3.0에서 0.3.1</h4>
1392
+ <p>애플리케이션 코드 변경은 필요하지 않습니다. <code>0.3.1</code>은 release hardening patch입니다. manual release tag 처리를 더 엄격하게 만들고, npm provenance 기대치를 문서화하며, security policy를 추가하고, GitHub publish workflow가 끝난 뒤 npm에 새 버전이 실제로 보이는지 확인합니다.</p>
1375
1393
  <hr>
1376
1394
  <h3 id="readme-ko-라이선스">라이선스</h3>
1377
1395
  <p>MIT License. 자세한 내용은 <a href="https://github.com/Feralthedogg/TypeSea/blob/main/LICENSE">LICENSE</a>를 보세요.</p>
@@ -1472,6 +1490,12 @@ const Node: Guard&lt;ListNode&gt; = t.lazy((): Guard&lt;ListNode&gt; =&gt;
1472
1490
  const result = Count.decode(&quot;42&quot;);
1473
1491
 
1474
1492
  const Name = t.default(t.string.min(1), &quot;anonymous&quot;);
1493
+ const NormalizedName = t.string
1494
+ .trim()
1495
+ .pipe(t.string.min(1))
1496
+ .transform((value) =&gt; value.toLowerCase())
1497
+ .default(&quot;anonymous&quot;)
1498
+ .catch(&quot;anonymous&quot;);
1475
1499
  const NumberText = t.codec(
1476
1500
  t.string.regex(/^\d+$/u, &quot;digits&quot;),
1477
1501
  t.number.int().nonnegative(),
@@ -1481,11 +1505,7 @@ const NumberText = t.codec(
1481
1505
  }
1482
1506
  );</code></pre>
1483
1507
  <p>Decoders are for output-producing operations. They return <code>Result</code> from <code>decode()</code> and do not expose <code>is()</code> predicates, because the decoded output may not be the same runtime value as the input.</p>
1484
- <ul><li><code>t.transform(source, mapper)</code> decodes <code>source</code>, then maps the decoded value.</li><li><code>t.pipe(source, next)</code> feeds a successful decoded value into the next guard or</li></ul>
1485
- <p>decoder.</p>
1486
- <ul><li><code>t.default(source, value)</code> returns a fallback output for <code>undefined</code> input.</li><li><code>t.prefault(source, value)</code> feeds a fallback input through the source.</li><li><code>t.codec(input, output, mapping)</code> validates both sides of a bidirectional</li></ul>
1487
- <p>decode/encode pair.</p>
1488
- <ul><li><code>t.coerce.string</code>, <code>t.coerce.number</code>, and <code>t.coerce.boolean</code> provide explicit</li></ul>
1508
+ <ul><li><code>t.transform(source, mapper)</code> decodes <code>source</code>, then maps the decoded value.</li><li><code>t.pipe(source, next)</code> feeds a successful decoded value into the next guard or decoder.</li><li><code>t.default(source, value)</code> returns a fallback output for <code>undefined</code> input.</li><li><code>t.prefault(source, value)</code> feeds a fallback input through the source.</li><li><code>t.catch(source, value)</code> returns a fallback output after a failed decode.</li><li><code>t.codec(input, output, mapping)</code> validates both sides of a bidirectional decode/encode pair.</li><li><code>t.coerce.string</code>, <code>t.coerce.number</code>, and <code>t.coerce.boolean</code> provide explicit</li></ul>
1489
1509
  <p>primitive coercion.</p>
1490
1510
  <ul><li><code>t.string.trim()</code>, <code>t.string.toLowerCase()</code>, and <code>t.string.toUpperCase()</code></li></ul>
1491
1511
  <p>are decoder helpers. They validate the string first, then return transformed output from <code>decode()</code>.</p>
@@ -1545,7 +1565,7 @@ const resolver = toReactHookFormResolver(User);</code></pre>
1545
1565
  <pre><code>const FastUser = compile(User);
1546
1566
  const fastParser = toTrpcParser(FastUser);
1547
1567
  const fastValidatorCompiler = toFastifyValidatorCompiler(FastUser);</code></pre>
1548
- <p>Use the default compiled mode at public input boundaries. For trusted, already-normalized internal data, the faster modes can be wired through adapters the same way.</p>
1568
+ <p>Use the default compiled mode at public input boundaries. It keeps the safe descriptor-read contract even when an adapter hides the direct <code>is()</code> call. For trusted, already-normalized internal data, the faster modes can be wired through adapters the same way.</p>
1549
1569
  <pre><code>const UnsafeUser = compile(User, { mode: &quot;unsafe&quot; });
1550
1570
  const internalParser = toTrpcParser(UnsafeUser);
1551
1571
 
@@ -1663,6 +1683,12 @@ const Node: Guard&lt;ListNode&gt; = t.lazy((): Guard&lt;ListNode&gt; =&gt;
1663
1683
  const result = Count.decode(&quot;42&quot;);
1664
1684
 
1665
1685
  const Name = t.default(t.string.min(1), &quot;anonymous&quot;);
1686
+ const NormalizedName = t.string
1687
+ .trim()
1688
+ .pipe(t.string.min(1))
1689
+ .transform((value) =&gt; value.toLowerCase())
1690
+ .default(&quot;anonymous&quot;)
1691
+ .catch(&quot;anonymous&quot;);
1666
1692
  const NumberText = t.codec(
1667
1693
  t.string.regex(/^\d+$/u, &quot;digits&quot;),
1668
1694
  t.number.int().nonnegative(),
@@ -1672,7 +1698,7 @@ const NumberText = t.codec(
1672
1698
  }
1673
1699
  );</code></pre>
1674
1700
  <p>decoder는 output을 생성하는 작업에 씁니다. <code>decode()</code>에서 <code>Result</code>를 반환하며 <code>is()</code> predicate를 노출하지 않습니다. decoded output이 input과 같은 runtime value가 아닐 수 있기 때문입니다.</p>
1675
- <ul><li><code>t.transform(source, mapper)</code>는 <code>source</code>를 decode한 뒤 decoded value를 map합니다.</li><li><code>t.pipe(source, next)</code>는 성공한 decoded value를 다음 guard 또는 decoder에 넘깁니다.</li><li><code>t.default(source, value)</code>는 input이 <code>undefined</code>일 때 fallback output을 바로 반환합니다.</li><li><code>t.prefault(source, value)</code>는 input이 <code>undefined</code>일 때 fallback input을 source에 다시 통과시킵니다.</li><li><code>t.codec(input, output, mapping)</code>은 bidirectional decode/encode 양쪽을 모두 검증합니다.</li><li><code>t.coerce.string</code>, <code>t.coerce.number</code>, <code>t.coerce.boolean</code>은 명시적 primitive coercion을 제공합니다.</li><li><code>t.string.trim()</code>, <code>t.string.toLowerCase()</code>, <code>t.string.toUpperCase()</code>는 decoder helper입니다. 먼저 string을 검증한 뒤 <code>decode()</code> 결과로 변환된 값을 반환합니다.</li><li><code>t.asyncRefine</code>, <code>t.asyncTransform</code>, <code>t.asyncPipe</code>는 <code>decodeAsync()</code>에서 <code>Promise&lt;Result&lt;T, Issue[]&gt;&gt;</code>를 반환합니다.</li></ul>
1701
+ <ul><li><code>t.transform(source, mapper)</code>는 <code>source</code>를 decode한 뒤 decoded value를 map합니다.</li><li><code>t.pipe(source, next)</code>는 성공한 decoded value를 다음 guard 또는 decoder에 넘깁니다.</li><li><code>t.default(source, value)</code>는 input이 <code>undefined</code>일 때 fallback output을 바로 반환합니다.</li><li><code>t.prefault(source, value)</code>는 input이 <code>undefined</code>일 때 fallback input을 source에 다시 통과시킵니다.</li><li><code>t.catch(source, value)</code>는 decode 실패 시 fallback output을 반환합니다.</li><li><code>t.codec(input, output, mapping)</code>은 bidirectional decode/encode 양쪽을 모두 검증합니다.</li><li><code>t.coerce.string</code>, <code>t.coerce.number</code>, <code>t.coerce.boolean</code>은 명시적 primitive coercion을 제공합니다.</li><li><code>t.string.trim()</code>, <code>t.string.toLowerCase()</code>, <code>t.string.toUpperCase()</code>는 decoder helper입니다. 먼저 string을 검증한 뒤 <code>decode()</code> 결과로 변환된 값을 반환합니다.</li><li><code>t.asyncRefine</code>, <code>t.asyncTransform</code>, <code>t.asyncPipe</code>는 <code>decodeAsync()</code>에서 <code>Promise&lt;Result&lt;T, Issue[]&gt;&gt;</code>를 반환합니다.</li></ul>
1676
1702
  <p>예상 가능한 async validation 실패도 <code>Result</code>로 반환됩니다.</p>
1677
1703
  <h3 id="api-reference-ko-message">Message</h3>
1678
1704
  <pre><code>const checked = withMessages(User.check(input), {
@@ -1725,7 +1751,7 @@ const resolver = toReactHookFormResolver(User);</code></pre>
1725
1751
  <pre><code>const FastUser = compile(User);
1726
1752
  const fastParser = toTrpcParser(FastUser);
1727
1753
  const fastValidatorCompiler = toFastifyValidatorCompiler(FastUser);</code></pre>
1728
- <p>public input boundary에서는 기본 compiled mode를 쓰세요. 신뢰된, 이미 정규화된 내부 데이터에서는 더 빠른 mode를 같은 방식으로 adapter에 연결할 수 있습니다.</p>
1754
+ <p>public input boundary에서는 기본 compiled mode를 쓰세요. adapter가 직접 <code>is()</code> 호출을 숨기더라도 safe descriptor-read 계약은 유지됩니다. 신뢰된, 이미 정규화된 내부 데이터에서는 더 빠른 mode를 같은 방식으로 adapter에 연결할 수 있습니다.</p>
1729
1755
  <pre><code>const UnsafeUser = compile(User, { mode: &quot;unsafe&quot; });
1730
1756
  const internalParser = toTrpcParser(UnsafeUser);
1731
1757
 
@@ -1822,11 +1848,11 @@ const optimized = optimizeGraph(graph);</code></pre>
1822
1848
  <ul><li><code>ecosystem.bench.ts</code> compares TypeSea runtime-plan, TypeSea compiled, Zod,</li></ul>
1823
1849
  <p>Valibot, and Ajv over one JSON-compatible strict-object contract.</p>
1824
1850
  <p>Zod, Valibot, and Ajv are dev dependencies for measurement only. They are not imported by <code>src</code>, and package policy rejects runtime, peer, optional, or bundled dependency fields before release.</p>
1825
- <p>Last local benchmark on 2026-07-04 KST reported these ecosystem paths over the JSON-compatible strict-object benchmark:</p>
1826
- <div class="table-wrap"><table><thead><tr><th>Case</th><th>TypeSea runtime plan</th><th>TypeSea compiled safe</th><th>TypeSea compiled unsafe</th><th>TypeSea compiled unchecked</th><th>Ajv compiled</th></tr></thead><tbody><tr><td>Valid <code>is()</code></td><td>513,701 hz</td><td>4,297,306 hz</td><td>36,297,653 hz</td><td>42,581,174 hz</td><td>4,275,389 hz</td></tr>
1827
- <tr><td>Valid <code>check()</code></td><td>503,232 hz</td><td>3,903,929 hz</td><td>35,568,425 hz</td><td>40,084,605 hz</td><td>4,278,587 hz</td></tr>
1828
- <tr><td>Invalid <code>is()</code></td><td>3,636,369 hz</td><td>42,080,241 hz</td><td>49,654,076 hz</td><td>50,482,732 hz</td><td>27,820,643 hz</td></tr>
1829
- <tr><td>Invalid <code>check()</code></td><td>420,446 hz</td><td>2,086,129 hz</td><td>3,077,367 hz</td><td>3,673,508 hz</td><td>28,713,035 hz</td></tr></tbody></table></div>
1851
+ <p>Last local benchmark on 2026-07-05 KST reported these ecosystem paths over the JSON-compatible strict-object benchmark:</p>
1852
+ <div class="table-wrap"><table><thead><tr><th>Case</th><th>TypeSea runtime plan</th><th>TypeSea compiled safe</th><th>TypeSea compiled unsafe</th><th>TypeSea compiled unchecked</th><th>Ajv compiled</th></tr></thead><tbody><tr><td>Valid <code>is()</code></td><td>478,576 hz</td><td>5,109,602 hz</td><td>36,777,097 hz</td><td>42,620,570 hz</td><td>4,238,036 hz</td></tr>
1853
+ <tr><td>Valid <code>check()</code></td><td>424,989 hz</td><td>4,642,948 hz</td><td>37,184,199 hz</td><td>42,487,325 hz</td><td>4,338,063 hz</td></tr>
1854
+ <tr><td>Invalid <code>is()</code></td><td>3,325,603 hz</td><td>43,094,061 hz</td><td>50,738,235 hz</td><td>50,898,012 hz</td><td>30,535,761 hz</td></tr>
1855
+ <tr><td>Invalid <code>check()</code></td><td>405,590 hz</td><td>2,107,460 hz</td><td>3,186,702 hz</td><td>3,509,673 hz</td><td>29,951,403 hz</td></tr></tbody></table></div>
1830
1856
  <p>Benchmark numbers are machine-local telemetry. They are useful for catching regressions, not for promising a fixed throughput floor. Unsafe and unchecked numbers are not hostile-input equivalent to safe mode.</p>
1831
1857
  </div>
1832
1858
  <div class="locale-ko" lang="ko">
@@ -1865,11 +1891,11 @@ const optimized = optimizeGraph(graph);</code></pre>
1865
1891
  <p>benchmark suite는 두 질문을 분리합니다.</p>
1866
1892
  <ul><li><code>compile.bench.ts</code>는 같은 TypeSea schema를 대상으로 TypeSea runtime-plan validator와 compiled validator를 비교합니다.</li><li><code>ecosystem.bench.ts</code>는 하나의 JSON-compatible strict-object contract를 대상으로 TypeSea runtime-plan, TypeSea compiled, Zod, Valibot, Ajv를 비교합니다.</li></ul>
1867
1893
  <p>Zod, Valibot, Ajv는 측정용 dev dependency입니다. <code>src</code>에서 import하지 않으며, package policy는 release 전에 runtime, peer, optional, bundled dependency field를 거부합니다.</p>
1868
- <p>2026-07-04 KST의 마지막 로컬 벤치마크는 JSON-compatible strict-object benchmark에서 아래 ecosystem path를 보고했습니다.</p>
1869
- <div class="table-wrap"><table><thead><tr><th>Case</th><th>TypeSea runtime plan</th><th>TypeSea compiled safe</th><th>TypeSea compiled unsafe</th><th>TypeSea compiled unchecked</th><th>Ajv compiled</th></tr></thead><tbody><tr><td>Valid <code>is()</code></td><td>513,701 hz</td><td>4,297,306 hz</td><td>36,297,653 hz</td><td>42,581,174 hz</td><td>4,275,389 hz</td></tr>
1870
- <tr><td>Valid <code>check()</code></td><td>503,232 hz</td><td>3,903,929 hz</td><td>35,568,425 hz</td><td>40,084,605 hz</td><td>4,278,587 hz</td></tr>
1871
- <tr><td>Invalid <code>is()</code></td><td>3,636,369 hz</td><td>42,080,241 hz</td><td>49,654,076 hz</td><td>50,482,732 hz</td><td>27,820,643 hz</td></tr>
1872
- <tr><td>Invalid <code>check()</code></td><td>420,446 hz</td><td>2,086,129 hz</td><td>3,077,367 hz</td><td>3,673,508 hz</td><td>28,713,035 hz</td></tr></tbody></table></div>
1894
+ <p>2026-07-05 KST의 마지막 로컬 벤치마크는 JSON-compatible strict-object benchmark에서 아래 ecosystem path를 보고했습니다.</p>
1895
+ <div class="table-wrap"><table><thead><tr><th>Case</th><th>TypeSea runtime plan</th><th>TypeSea compiled safe</th><th>TypeSea compiled unsafe</th><th>TypeSea compiled unchecked</th><th>Ajv compiled</th></tr></thead><tbody><tr><td>Valid <code>is()</code></td><td>478,576 hz</td><td>5,109,602 hz</td><td>36,777,097 hz</td><td>42,620,570 hz</td><td>4,238,036 hz</td></tr>
1896
+ <tr><td>Valid <code>check()</code></td><td>424,989 hz</td><td>4,642,948 hz</td><td>37,184,199 hz</td><td>42,487,325 hz</td><td>4,338,063 hz</td></tr>
1897
+ <tr><td>Invalid <code>is()</code></td><td>3,325,603 hz</td><td>43,094,061 hz</td><td>50,738,235 hz</td><td>50,898,012 hz</td><td>30,535,761 hz</td></tr>
1898
+ <tr><td>Invalid <code>check()</code></td><td>405,590 hz</td><td>2,107,460 hz</td><td>3,186,702 hz</td><td>3,509,673 hz</td><td>29,951,403 hz</td></tr></tbody></table></div>
1873
1899
  <p>benchmark number는 machine-local telemetry입니다. regression을 잡는 데 유용하지만 고정된 throughput floor를 약속하지 않습니다. unsafe와 unchecked number는 safe mode와 hostile-input equivalent가 아닙니다.</p>
1874
1900
  </div>
1875
1901
  </article>
package/docs/ko/api.md CHANGED
@@ -135,6 +135,12 @@ const Count = t.pipe(t.coerce.number(), t.number.int().gte(0));
135
135
  const result = Count.decode("42");
136
136
 
137
137
  const Name = t.default(t.string.min(1), "anonymous");
138
+ const NormalizedName = t.string
139
+ .trim()
140
+ .pipe(t.string.min(1))
141
+ .transform((value) => value.toLowerCase())
142
+ .default("anonymous")
143
+ .catch("anonymous");
138
144
  const NumberText = t.codec(
139
145
  t.string.regex(/^\d+$/u, "digits"),
140
146
  t.number.int().nonnegative(),
@@ -153,6 +159,7 @@ decoded output이 input과 같은 runtime value가 아닐 수 있기 때문입
153
159
  - `t.pipe(source, next)`는 성공한 decoded value를 다음 guard 또는 decoder에 넘깁니다.
154
160
  - `t.default(source, value)`는 input이 `undefined`일 때 fallback output을 바로 반환합니다.
155
161
  - `t.prefault(source, value)`는 input이 `undefined`일 때 fallback input을 source에 다시 통과시킵니다.
162
+ - `t.catch(source, value)`는 decode 실패 시 fallback output을 반환합니다.
156
163
  - `t.codec(input, output, mapping)`은 bidirectional decode/encode 양쪽을 모두 검증합니다.
157
164
  - `t.coerce.string`, `t.coerce.number`, `t.coerce.boolean`은 명시적 primitive coercion을 제공합니다.
158
165
  - `t.string.trim()`, `t.string.toLowerCase()`, `t.string.toUpperCase()`는 decoder helper입니다. 먼저 string을 검증한 뒤 `decode()` 결과로 변환된 값을 반환합니다.
@@ -287,6 +294,7 @@ const fastValidatorCompiler = toFastifyValidatorCompiler(FastUser);
287
294
  ```
288
295
 
289
296
  public input boundary에서는 기본 compiled mode를 쓰세요.
297
+ adapter가 직접 `is()` 호출을 숨기더라도 safe descriptor-read 계약은 유지됩니다.
290
298
  신뢰된, 이미 정규화된 내부 데이터에서는 더 빠른 mode를 같은 방식으로 adapter에 연결할 수 있습니다.
291
299
 
292
300
  ```ts
@@ -142,14 +142,14 @@ benchmark suite는 두 질문을 분리합니다.
142
142
  Zod, Valibot, Ajv는 측정용 dev dependency입니다.
143
143
  `src`에서 import하지 않으며, package policy는 release 전에 runtime, peer, optional, bundled dependency field를 거부합니다.
144
144
 
145
- 2026-07-04 KST의 마지막 로컬 벤치마크는 JSON-compatible strict-object benchmark에서 아래 ecosystem path를 보고했습니다.
145
+ 2026-07-05 KST의 마지막 로컬 벤치마크는 JSON-compatible strict-object benchmark에서 아래 ecosystem path를 보고했습니다.
146
146
 
147
147
  | Case | TypeSea runtime plan | TypeSea compiled safe | TypeSea compiled unsafe | TypeSea compiled unchecked | Ajv compiled |
148
148
  | --- | ---: | ---: | ---: | ---: | ---: |
149
- | Valid `is()` | 513,701 hz | 4,297,306 hz | 36,297,653 hz | 42,581,174 hz | 4,275,389 hz |
150
- | Valid `check()` | 503,232 hz | 3,903,929 hz | 35,568,425 hz | 40,084,605 hz | 4,278,587 hz |
151
- | Invalid `is()` | 3,636,369 hz | 42,080,241 hz | 49,654,076 hz | 50,482,732 hz | 27,820,643 hz |
152
- | Invalid `check()` | 420,446 hz | 2,086,129 hz | 3,077,367 hz | 3,673,508 hz | 28,713,035 hz |
149
+ | Valid `is()` | 478,576 hz | 5,109,602 hz | 36,777,097 hz | 42,620,570 hz | 4,238,036 hz |
150
+ | Valid `check()` | 424,989 hz | 4,642,948 hz | 37,184,199 hz | 42,487,325 hz | 4,338,063 hz |
151
+ | Invalid `is()` | 3,325,603 hz | 43,094,061 hz | 50,738,235 hz | 50,898,012 hz | 30,535,761 hz |
152
+ | Invalid `check()` | 405,590 hz | 2,107,460 hz | 3,186,702 hz | 3,509,673 hz | 29,951,403 hz |
153
153
 
154
154
  benchmark number는 machine-local telemetry입니다.
155
155
  regression을 잡는 데 유용하지만 고정된 throughput floor를 약속하지 않습니다.
package/docs/ko/readme.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## 벤치마크 요약
7
7
 
8
- 마지막 로컬 벤치마크는 2026-07-04 KST에 실행했습니다.
8
+ 마지막 로컬 벤치마크는 2026-07-05 KST에 실행했습니다.
9
9
  명령은 `npm run bench -- bench/ecosystem.bench.ts --run`이며, strict object 계약을 대상으로 한 단일 머신의 초당 실행 횟수입니다.
10
10
  아래 수치는 회귀를 잡기 위한 로컬 측정값이지, 릴리스 성능 보증값은 아닙니다.
11
11
 
@@ -26,6 +26,12 @@ TypeSea의 안전 모드 컴파일 검증기는 getter 실행 방지와 strict e
26
26
  > 예상 가능한 실패는 동결된 `Result`로 반환합니다.
27
27
  > 불명확한 타입 탈출과 암묵적 예외 흐름에 기대지 않도록 코드베이스 전체에 정책 게이트를 둡니다.
28
28
 
29
+ > [!WARNING]
30
+ > `unsafe`와 `unchecked`는 **public boundary용 모드가 아닙니다**.
31
+ > 이미 신뢰 가능한 plain data로 정규화된 입력에서만 사용하세요.
32
+ > 이 모드에서는 getter 실행, prototype-backed value 수용, 더 약한 strict extra-key 보장을 호출자가 받아들이는 것입니다.
33
+ > 외부 입력에는 기본 safe mode를 쓰는 것이 TypeSea의 보안 계약입니다.
34
+
29
35
  ---
30
36
 
31
37
  ## 왜 만들었나
@@ -178,49 +184,49 @@ failed check() -> schema-aware diagnostic collector
178
184
 
179
185
  ## 성능 스냅샷
180
186
 
181
- 마지막 로컬 벤치마크는 2026-07-04 KST에 실행했습니다.
187
+ 마지막 로컬 벤치마크는 2026-07-05 KST에 실행했습니다.
182
188
  `npm run bench -- bench/ecosystem.bench.ts --run`을 사용했고, benchmark strict-object 계약을 대상으로 했습니다.
183
189
  아래 값은 단일 머신의 초당 실행 횟수이며 릴리스 성능 보증값은 아닙니다.
184
190
 
185
191
  | 유효한 객체: boolean 경로 | hz |
186
192
  | --- | ---: |
187
- | TypeSea interpreted `is()` | 513,701 |
188
- | TypeSea compiled safe `is()` | 4,297,306 |
189
- | TypeSea compiled unsafe `is()` | 36,297,653 |
190
- | TypeSea compiled unchecked `is()` | 42,581,174 |
191
- | Zod `safeParse` | 1,343,756 |
192
- | Valibot `safeParse` | 1,406,528 |
193
- | Ajv compiled | 4,275,389 |
193
+ | TypeSea interpreted `is()` | 478,576 |
194
+ | TypeSea compiled safe `is()` | 5,109,602 |
195
+ | TypeSea compiled unsafe `is()` | 36,777,097 |
196
+ | TypeSea compiled unchecked `is()` | 42,620,570 |
197
+ | Zod `safeParse` | 1,400,045 |
198
+ | Valibot `safeParse` | 1,400,599 |
199
+ | Ajv compiled | 4,238,036 |
194
200
 
195
201
  | 유효한 객체: 진단 경로 | hz |
196
202
  | --- | ---: |
197
- | TypeSea interpreted `check()` | 503,232 |
198
- | TypeSea compiled safe `check()` | 3,903,929 |
199
- | TypeSea compiled unsafe `check()` | 35,568,425 |
200
- | TypeSea compiled unchecked `check()` | 40,084,605 |
201
- | Zod `safeParse` | 1,355,014 |
202
- | Valibot `safeParse` | 1,378,266 |
203
- | Ajv compiled | 4,278,587 |
203
+ | TypeSea interpreted `check()` | 424,989 |
204
+ | TypeSea compiled safe `check()` | 4,642,948 |
205
+ | TypeSea compiled unsafe `check()` | 37,184,199 |
206
+ | TypeSea compiled unchecked `check()` | 42,487,325 |
207
+ | Zod `safeParse` | 1,278,859 |
208
+ | Valibot `safeParse` | 1,391,040 |
209
+ | Ajv compiled | 4,338,063 |
204
210
 
205
211
  | 잘못된 객체: boolean 경로 | hz |
206
212
  | --- | ---: |
207
- | TypeSea interpreted `is()` | 3,636,369 |
208
- | TypeSea compiled safe `is()` | 42,080,241 |
209
- | TypeSea compiled unsafe `is()` | 49,654,076 |
210
- | TypeSea compiled unchecked `is()` | 50,482,732 |
211
- | Zod `safeParse` | 84,272 |
212
- | Valibot `safeParse` | 878,521 |
213
- | Ajv compiled | 27,820,643 |
213
+ | TypeSea interpreted `is()` | 3,325,603 |
214
+ | TypeSea compiled safe `is()` | 43,094,061 |
215
+ | TypeSea compiled unsafe `is()` | 50,738,235 |
216
+ | TypeSea compiled unchecked `is()` | 50,898,012 |
217
+ | Zod `safeParse` | 84,647 |
218
+ | Valibot `safeParse` | 866,013 |
219
+ | Ajv compiled | 30,535,761 |
214
220
 
215
221
  | 잘못된 객체: 진단 경로 | hz |
216
222
  | --- | ---: |
217
- | TypeSea interpreted `check()` | 420,446 |
218
- | TypeSea compiled safe `check()` | 2,086,129 |
219
- | TypeSea compiled unsafe `check()` | 3,077,367 |
220
- | TypeSea compiled unchecked `check()` | 3,673,508 |
221
- | Zod `safeParse` | 79,613 |
222
- | Valibot `safeParse` | 887,991 |
223
- | Ajv compiled | 28,713,035 |
223
+ | TypeSea interpreted `check()` | 405,590 |
224
+ | TypeSea compiled safe `check()` | 2,107,460 |
225
+ | TypeSea compiled unsafe `check()` | 3,186,702 |
226
+ | TypeSea compiled unchecked `check()` | 3,509,673 |
227
+ | Zod `safeParse` | 85,355 |
228
+ | Valibot `safeParse` | 788,870 |
229
+ | Ajv compiled | 29,951,403 |
224
230
 
225
231
  safe compiled path는 TypeSea의 적대적 입력 방어를 유지하면서 Ajv에 가깝게 동작합니다.
226
232
  descriptor 기반 property read, symbol/non-enumerable strict-key rejection, key presence semantics, immutable diagnostics, TypeScript guard inference를 유지합니다.
@@ -352,12 +358,21 @@ npm run check:consumer # tarball install + runtime/type smoke in a temp project
352
358
  npm run bench -- --run # benchmark smoke
353
359
  npm run pack:dry # package contents dry run
354
360
  npm run release:check # the full pre-publish gate
361
+ npm run release:publish # provenance를 붙이고 lifecycle script를 무시하는 npm publish
355
362
  ```
356
363
 
357
364
  `npm run release:check`는 publish 전에 기대하는 동일한 gate를 실행합니다.
358
365
  typecheck, lint, tests, build, docs smoke, dist policy, public API snapshot, package contents, consumer install, benchmark smoke, pack dry run을 포함합니다.
359
366
  CI는 Node 20.19, 22, 24에서 실행하고, release는 npm provenance와 함께 publish합니다.
360
367
 
368
+ 릴리스 경로:
369
+
370
+ 1. `vX.Y.Z` 태그를 push하거나 GitHub `Release` workflow를 그 태그로 실행합니다.
371
+ 2. release workflow는 tag가 `package.json`의 version과 일치하는지 확인합니다.
372
+ 3. publish는 GitHub `Publish` workflow에서 `npm run release:publish`로 수행합니다. 이 스크립트는 `npm publish --provenance --access public --ignore-scripts`로 확장됩니다.
373
+
374
+ 로컬 `NPM_TOKEN` publish는 수동 복구 릴리스용입니다. 이 경우에도 먼저 `npm run release:check`를 통과해야 하며, GitHub OIDC provenance는 붙지 않습니다.
375
+
361
376
  > [!NOTE]
362
377
  > benchmark 비교 패키지인 Zod, Valibot, Ajv는 dev dependency일 뿐입니다.
363
378
  > package policy는 이들이 runtime dependency field에 들어가는 것을 거부합니다.
@@ -370,6 +385,17 @@ CI는 Node 20.19, 22, 24에서 실행하고, release는 npm provenance와 함께
370
385
  - [문서 사이트](https://feralthedogg.github.io/TypeSea/)
371
386
  - [API 레퍼런스](../api.md)
372
387
  - [엔진 노트](../engine-notes.md)
388
+ - [보안 정책](https://github.com/Feralthedogg/TypeSea/blob/main/SECURITY.md)
389
+
390
+ ---
391
+
392
+ ## 마이그레이션 노트
393
+
394
+ ### 0.3.0에서 0.3.1
395
+
396
+ 애플리케이션 코드 변경은 필요하지 않습니다.
397
+ `0.3.1`은 release hardening patch입니다.
398
+ manual release tag 처리를 더 엄격하게 만들고, npm provenance 기대치를 문서화하며, security policy를 추가하고, GitHub publish workflow가 끝난 뒤 npm에 새 버전이 실제로 보이는지 확인합니다.
373
399
 
374
400
  ---
375
401
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typesea",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "V8-friendly runtime narrowing for TypeScript with Sea-of-Nodes graph introspection.",
5
5
  "type": "module",
6
6
  "author": "feral",
@@ -30,6 +30,7 @@
30
30
  "files": [
31
31
  "dist",
32
32
  "README.md",
33
+ "SECURITY.md",
33
34
  "CHANGELOG.md",
34
35
  "docs"
35
36
  ],
@@ -48,6 +49,8 @@
48
49
  "policy": "node scripts/source-policy.mjs",
49
50
  "prepack": "npm run check",
50
51
  "release:check": "node scripts/release-gate.mjs",
52
+ "release:publish": "npm publish --provenance --access public --ignore-scripts",
53
+ "release:verify-published": "node scripts/verify-published-version.mjs",
51
54
  "test": "vitest run",
52
55
  "typecheck": "tsc -p tsconfig.json --noEmit"
53
56
  },