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 +22 -0
- package/README.md +60 -31
- package/SECURITY.md +52 -0
- package/docs/api.md +13 -7
- package/docs/assets/benchmark-headline.svg +33 -33
- package/docs/engine-notes.md +5 -5
- package/docs/index.html +110 -84
- package/docs/ko/api.md +8 -0
- package/docs/ko/engine-notes.md +5 -5
- package/docs/ko/readme.md +56 -30
- package/package.json +4 -1
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
|
[](https://github.com/Feralthedogg/TypeSea/actions/workflows/ci.yml)
|
|
4
|
-
[](https://socket.dev/npm/package/typesea)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|

|
|
7
7
|

|
|
@@ -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-
|
|
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-
|
|
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()` |
|
|
233
|
-
| TypeSea compiled safe `is()` |
|
|
234
|
-
| TypeSea compiled unsafe `is()` | 36,
|
|
235
|
-
| TypeSea compiled unchecked `is()` | 42,
|
|
236
|
-
| Zod `safeParse` | 1,
|
|
237
|
-
| Valibot `safeParse` | 1,
|
|
238
|
-
| Ajv compiled | 4,
|
|
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()` |
|
|
243
|
-
| TypeSea compiled safe `check()` |
|
|
244
|
-
| TypeSea compiled unsafe `check()` |
|
|
245
|
-
| TypeSea compiled unchecked `check()` |
|
|
246
|
-
| Zod `safeParse` | 1,
|
|
247
|
-
| Valibot `safeParse` | 1,
|
|
248
|
-
| Ajv compiled | 4,
|
|
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,
|
|
253
|
-
| TypeSea compiled safe `is()` |
|
|
254
|
-
| TypeSea compiled unsafe `is()` |
|
|
255
|
-
| TypeSea compiled unchecked `is()` | 50,
|
|
256
|
-
| Zod `safeParse` | 84,
|
|
257
|
-
| Valibot `safeParse` |
|
|
258
|
-
| Ajv compiled |
|
|
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()` |
|
|
263
|
-
| TypeSea compiled safe `check()` | 2,
|
|
264
|
-
| TypeSea compiled unsafe `check()` | 3,
|
|
265
|
-
| TypeSea compiled unchecked `check()` | 3,673
|
|
266
|
-
| Zod `safeParse` |
|
|
267
|
-
| Valibot `safeParse` |
|
|
268
|
-
| Ajv compiled |
|
|
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.
|
|
171
|
-
|
|
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.
|
|
324
|
-
|
|
325
|
-
the
|
|
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-
|
|
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.
|
|
67
|
-
<text x="66" y="210" class="metric-sub">
|
|
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">
|
|
72
|
-
<text x="421" y="210" class="metric-sub">1.
|
|
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">
|
|
77
|
-
<text x="776" y="210" class="metric-sub">Ajv
|
|
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.
|
|
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.
|
|
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="
|
|
92
|
-
<text x="
|
|
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="
|
|
96
|
-
<rect x="
|
|
97
|
-
<rect x="
|
|
98
|
-
<rect x="
|
|
99
|
-
<text x="
|
|
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.
|
|
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.
|
|
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="
|
|
114
|
-
<text x="1024" y="506" class="bar-value">
|
|
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="
|
|
118
|
-
<text x="
|
|
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="
|
|
122
|
-
<rect x="
|
|
123
|
-
<rect x="
|
|
124
|
-
<text x="
|
|
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
|
|
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">
|
|
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="
|
|
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="
|
|
141
|
-
<text x="532" y="666" class="bar-value">unchecked 3.
|
|
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="
|
|
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.
|
|
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)">
|
package/docs/engine-notes.md
CHANGED
|
@@ -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-
|
|
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()` |
|
|
194
|
-
| Valid `check()` |
|
|
195
|
-
| Invalid `is()` | 3,
|
|
196
|
-
| Invalid `check()` |
|
|
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.
|
|
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"> <span class="muted-link"> <span class="muted-link"> <span class="muted-link"> <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-
|
|
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 "probably valid", 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() -> 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-
|
|
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>
|
|
1064
|
-
<tr><td>TypeSea compiled safe <code>is()</code></td><td>
|
|
1065
|
-
<tr><td>TypeSea compiled unsafe <code>is()</code></td><td>36,
|
|
1066
|
-
<tr><td>TypeSea compiled unchecked <code>is()</code></td><td>42,
|
|
1067
|
-
<tr><td>Zod <code>safeParse</code></td><td>1,
|
|
1068
|
-
<tr><td>Valibot <code>safeParse</code></td><td>1,
|
|
1069
|
-
<tr><td>Ajv compiled</td><td>4,
|
|
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>
|
|
1071
|
-
<tr><td>TypeSea compiled safe <code>check()</code></td><td>
|
|
1072
|
-
<tr><td>TypeSea compiled unsafe <code>check()</code></td><td>
|
|
1073
|
-
<tr><td>TypeSea compiled unchecked <code>check()</code></td><td>
|
|
1074
|
-
<tr><td>Zod <code>safeParse</code></td><td>1,
|
|
1075
|
-
<tr><td>Valibot <code>safeParse</code></td><td>1,
|
|
1076
|
-
<tr><td>Ajv compiled</td><td>4,
|
|
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,
|
|
1078
|
-
<tr><td>TypeSea compiled safe <code>is()</code></td><td>
|
|
1079
|
-
<tr><td>TypeSea compiled unsafe <code>is()</code></td><td>
|
|
1080
|
-
<tr><td>TypeSea compiled unchecked <code>is()</code></td><td>50,
|
|
1081
|
-
<tr><td>Zod <code>safeParse</code></td><td>84,
|
|
1082
|
-
<tr><td>Valibot <code>safeParse</code></td><td>
|
|
1083
|
-
<tr><td>Ajv compiled</td><td>
|
|
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>
|
|
1085
|
-
<tr><td>TypeSea compiled safe <code>check()</code></td><td>2,
|
|
1086
|
-
<tr><td>TypeSea compiled unsafe <code>check()</code></td><td>3,
|
|
1087
|
-
<tr><td>TypeSea compiled unchecked <code>check()</code></td><td>3,673
|
|
1088
|
-
<tr><td>Zod <code>safeParse</code></td><td>
|
|
1089
|
-
<tr><td>Valibot <code>safeParse</code></td><td>
|
|
1090
|
-
<tr><td>Ajv compiled</td><td>
|
|
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<ListNode> = t.lazy((): Guard<ListNode> =>
|
|
|
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)
|
|
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-
|
|
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>목표는 "대충 유효해 보이면 통과"가 아닙니다. 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() -> 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-
|
|
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>
|
|
1268
|
-
<tr><td>TypeSea compiled safe <code>is()</code></td><td>
|
|
1269
|
-
<tr><td>TypeSea compiled unsafe <code>is()</code></td><td>36,
|
|
1270
|
-
<tr><td>TypeSea compiled unchecked <code>is()</code></td><td>42,
|
|
1271
|
-
<tr><td>Zod <code>safeParse</code></td><td>1,
|
|
1272
|
-
<tr><td>Valibot <code>safeParse</code></td><td>1,
|
|
1273
|
-
<tr><td>Ajv compiled</td><td>4,
|
|
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>
|
|
1275
|
-
<tr><td>TypeSea compiled safe <code>check()</code></td><td>
|
|
1276
|
-
<tr><td>TypeSea compiled unsafe <code>check()</code></td><td>
|
|
1277
|
-
<tr><td>TypeSea compiled unchecked <code>check()</code></td><td>
|
|
1278
|
-
<tr><td>Zod <code>safeParse</code></td><td>1,
|
|
1279
|
-
<tr><td>Valibot <code>safeParse</code></td><td>1,
|
|
1280
|
-
<tr><td>Ajv compiled</td><td>4,
|
|
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,
|
|
1282
|
-
<tr><td>TypeSea compiled safe <code>is()</code></td><td>
|
|
1283
|
-
<tr><td>TypeSea compiled unsafe <code>is()</code></td><td>
|
|
1284
|
-
<tr><td>TypeSea compiled unchecked <code>is()</code></td><td>50,
|
|
1285
|
-
<tr><td>Zod <code>safeParse</code></td><td>84,
|
|
1286
|
-
<tr><td>Valibot <code>safeParse</code></td><td>
|
|
1287
|
-
<tr><td>Ajv compiled</td><td>
|
|
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>
|
|
1289
|
-
<tr><td>TypeSea compiled safe <code>check()</code></td><td>2,
|
|
1290
|
-
<tr><td>TypeSea compiled unsafe <code>check()</code></td><td>3,
|
|
1291
|
-
<tr><td>TypeSea compiled unchecked <code>check()</code></td><td>3,673
|
|
1292
|
-
<tr><td>Zod <code>safeParse</code></td><td>
|
|
1293
|
-
<tr><td>Valibot <code>safeParse</code></td><td>
|
|
1294
|
-
<tr><td>Ajv compiled</td><td>
|
|
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<ListNode> = t.lazy((): Guard<ListNode> =>
|
|
|
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
|
|
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<ListNode> = t.lazy((): Guard<ListNode> =>
|
|
|
1472
1490
|
const result = Count.decode("42");
|
|
1473
1491
|
|
|
1474
1492
|
const Name = t.default(t.string.min(1), "anonymous");
|
|
1493
|
+
const NormalizedName = t.string
|
|
1494
|
+
.trim()
|
|
1495
|
+
.pipe(t.string.min(1))
|
|
1496
|
+
.transform((value) => value.toLowerCase())
|
|
1497
|
+
.default("anonymous")
|
|
1498
|
+
.catch("anonymous");
|
|
1475
1499
|
const NumberText = t.codec(
|
|
1476
1500
|
t.string.regex(/^\d+$/u, "digits"),
|
|
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: "unsafe" });
|
|
1550
1570
|
const internalParser = toTrpcParser(UnsafeUser);
|
|
1551
1571
|
|
|
@@ -1663,6 +1683,12 @@ const Node: Guard<ListNode> = t.lazy((): Guard<ListNode> =>
|
|
|
1663
1683
|
const result = Count.decode("42");
|
|
1664
1684
|
|
|
1665
1685
|
const Name = t.default(t.string.min(1), "anonymous");
|
|
1686
|
+
const NormalizedName = t.string
|
|
1687
|
+
.trim()
|
|
1688
|
+
.pipe(t.string.min(1))
|
|
1689
|
+
.transform((value) => value.toLowerCase())
|
|
1690
|
+
.default("anonymous")
|
|
1691
|
+
.catch("anonymous");
|
|
1666
1692
|
const NumberText = t.codec(
|
|
1667
1693
|
t.string.regex(/^\d+$/u, "digits"),
|
|
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<Result<T, Issue[]>></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<Result<T, Issue[]>></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: "unsafe" });
|
|
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-
|
|
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>
|
|
1827
|
-
<tr><td>Valid <code>check()</code></td><td>
|
|
1828
|
-
<tr><td>Invalid <code>is()</code></td><td>3,
|
|
1829
|
-
<tr><td>Invalid <code>check()</code></td><td>
|
|
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-
|
|
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>
|
|
1870
|
-
<tr><td>Valid <code>check()</code></td><td>
|
|
1871
|
-
<tr><td>Invalid <code>is()</code></td><td>3,
|
|
1872
|
-
<tr><td>Invalid <code>check()</code></td><td>
|
|
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
|
package/docs/ko/engine-notes.md
CHANGED
|
@@ -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-
|
|
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()` |
|
|
150
|
-
| Valid `check()` |
|
|
151
|
-
| Invalid `is()` | 3,
|
|
152
|
-
| Invalid `check()` |
|
|
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-
|
|
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-
|
|
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()` |
|
|
188
|
-
| TypeSea compiled safe `is()` |
|
|
189
|
-
| TypeSea compiled unsafe `is()` | 36,
|
|
190
|
-
| TypeSea compiled unchecked `is()` | 42,
|
|
191
|
-
| Zod `safeParse` | 1,
|
|
192
|
-
| Valibot `safeParse` | 1,
|
|
193
|
-
| Ajv compiled | 4,
|
|
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()` |
|
|
198
|
-
| TypeSea compiled safe `check()` |
|
|
199
|
-
| TypeSea compiled unsafe `check()` |
|
|
200
|
-
| TypeSea compiled unchecked `check()` |
|
|
201
|
-
| Zod `safeParse` | 1,
|
|
202
|
-
| Valibot `safeParse` | 1,
|
|
203
|
-
| Ajv compiled | 4,
|
|
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,
|
|
208
|
-
| TypeSea compiled safe `is()` |
|
|
209
|
-
| TypeSea compiled unsafe `is()` |
|
|
210
|
-
| TypeSea compiled unchecked `is()` | 50,
|
|
211
|
-
| Zod `safeParse` | 84,
|
|
212
|
-
| Valibot `safeParse` |
|
|
213
|
-
| Ajv compiled |
|
|
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()` |
|
|
218
|
-
| TypeSea compiled safe `check()` | 2,
|
|
219
|
-
| TypeSea compiled unsafe `check()` | 3,
|
|
220
|
-
| TypeSea compiled unchecked `check()` | 3,673
|
|
221
|
-
| Zod `safeParse` |
|
|
222
|
-
| Valibot `safeParse` |
|
|
223
|
-
| Ajv compiled |
|
|
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.
|
|
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
|
},
|