@taprootio/docs-artifact 1.0.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/LICENSE +13 -0
- package/README.md +362 -0
- package/bin/taproot-docs-conformance.js +20 -0
- package/bin/taproot-docs-validate.js +17 -0
- package/conformance.d.ts +11 -0
- package/fixtures/README.md +24 -0
- package/fixtures/conformance.json +1630 -0
- package/fixtures/invalid/duplicate-json-key.json +1 -0
- package/fixtures/invalid/hash-drift/taproot-docs/fragments/welcome.html +1 -0
- package/fixtures/invalid/size-drift/taproot-docs/fragments/welcome.html +1 -0
- package/fixtures/invalid/unsafe-markup/taproot-docs/fragments/welcome.html +1 -0
- package/fixtures/valid/complete/taproot-docs/assets/pixel.png.base64 +1 -0
- package/fixtures/valid/complete/taproot-docs/fragments/button.en-us.html +1 -0
- package/fixtures/valid/complete/taproot-docs/fragments/button.fr-fr.html +1 -0
- package/fixtures/valid/complete/taproot-docs/fragments/getting-started.en-us.html +3 -0
- package/fixtures/valid/complete/taproot-docs/fragments/getting-started.fr-fr.html +3 -0
- package/fixtures/valid/complete/taproot-docs-manifest.json +231 -0
- package/fixtures/valid/minimal/taproot-docs/fragments/welcome.html +1 -0
- package/fixtures/valid/minimal/taproot-docs-manifest.json +80 -0
- package/index.d.ts +204 -0
- package/node.d.ts +6 -0
- package/package.json +54 -0
- package/schema/taproot-docs-manifest.schema.json +487 -0
- package/src/artifact-validator.js +870 -0
- package/src/binary.js +67 -0
- package/src/conformance.js +578 -0
- package/src/constants.js +104 -0
- package/src/errors.js +139 -0
- package/src/index.js +18 -0
- package/src/json.js +516 -0
- package/src/manifest-validator.js +650 -0
- package/src/markup.js +578 -0
- package/src/node-internal.js +4 -0
- package/src/node.js +513 -0
- package/src/path.js +103 -0
- package/src/text.js +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright (c) 2026 Taproot IO, LLC
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
4
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
5
|
+
copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
8
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
9
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
10
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
11
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
12
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
|
13
|
+
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# `@taprootio/docs-artifact`
|
|
2
|
+
|
|
3
|
+
The canonical, independently versioned contract between documentation producers
|
|
4
|
+
and Taproot Docs consumers. The public npm package contains all four authorities
|
|
5
|
+
that must move together:
|
|
6
|
+
|
|
7
|
+
- `schema/taproot-docs-manifest.schema.json` for the structural v1 shape;
|
|
8
|
+
- the validator and deterministic serializer exported from the package root;
|
|
9
|
+
- the Node directory validator and `taproot-docs-validate` command;
|
|
10
|
+
- valid and adversarial fixtures exposed by `@taprootio/docs-artifact/conformance`.
|
|
11
|
+
|
|
12
|
+
WTFM and Taproot must pin the same exact released package version. Neither
|
|
13
|
+
repository copies the schema, path rules, markup allow-list, limits, or fixture
|
|
14
|
+
data.
|
|
15
|
+
|
|
16
|
+
## Artifact layout and portability
|
|
17
|
+
|
|
18
|
+
The producer writes this additive payload into its ordinary static output:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
_site/
|
|
22
|
+
├── index.html ordinary portable static site
|
|
23
|
+
├── ... ordinary CSS, JS, pages, and assets
|
|
24
|
+
├── taproot-docs-manifest.json semantic contract entry point
|
|
25
|
+
└── taproot-docs/
|
|
26
|
+
├── fragments/ manifest-listed semantic HTML only
|
|
27
|
+
└── assets/ manifest-listed safe raster images only
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Taproot packages and validates the manifest plus exactly the files it lists. It
|
|
31
|
+
does not ingest, scrape, or execute the ordinary final HTML, CSS, or JavaScript.
|
|
32
|
+
`validateArtifactDirectory()` deliberately ignores unlisted files outside the
|
|
33
|
+
semantic payload, so the complete build remains deployable to any normal static
|
|
34
|
+
host. It enumerates the entire `taproot-docs/` subtree and rejects undeclared
|
|
35
|
+
files, symbolic links, and unsupported filesystem entry types there. A publisher
|
|
36
|
+
creating the Taproot upload must include only the manifest and listed semantic
|
|
37
|
+
files; `validateArtifact()` likewise rejects unexpected entries in that managed
|
|
38
|
+
subset.
|
|
39
|
+
|
|
40
|
+
## Identity and document model
|
|
41
|
+
|
|
42
|
+
`resources[].key` is the durable, locale-independent document identity. It is a
|
|
43
|
+
producer-assigned lowercase logical key such as `guide:getting-started` or
|
|
44
|
+
`reference:esp-button`. It must not be derived again from `route`, `title`, or
|
|
45
|
+
array position. A route or title can change while the key remains fixed, which
|
|
46
|
+
lets Taproot preserve analytics and future feedback identity.
|
|
47
|
+
|
|
48
|
+
Each resource has:
|
|
49
|
+
|
|
50
|
+
- semantic kind, audience, and tags;
|
|
51
|
+
- one or more locale variants, including the declared default locale;
|
|
52
|
+
- a canonical route, title, description, ordered heading inventory, and source
|
|
53
|
+
location for each locale;
|
|
54
|
+
- one body fragment plus optional example or aside fragments, all carrying
|
|
55
|
+
exact UTF-8 byte counts and SHA-256 hashes.
|
|
56
|
+
|
|
57
|
+
Navigation is localized and references resource keys rather than routes.
|
|
58
|
+
Redirects map a canonical old route directly to a resource key and locale, so
|
|
59
|
+
the target is always a current resource variant rather than another redirect.
|
|
60
|
+
This excludes redirect chains and loops by construction. Assets have their own
|
|
61
|
+
stable keys, paths, exact byte counts, hashes, media types, and dimensions.
|
|
62
|
+
|
|
63
|
+
Repository provenance uses GitHub's stable repository id as authority and keeps
|
|
64
|
+
the owner/name locator for humans. The exact revision, ref, producer/version,
|
|
65
|
+
configuration hash, and `sourceDateEpoch` make build inputs inspectable without
|
|
66
|
+
introducing a wall-clock value that changes identical output.
|
|
67
|
+
|
|
68
|
+
## Compatibility policy
|
|
69
|
+
|
|
70
|
+
The manifest `schemaVersion` is the wire-format major and v1 consumers accept
|
|
71
|
+
only `1`. Unknown schema versions fail closed. Within v1:
|
|
72
|
+
|
|
73
|
+
- additive behavior is advertised through `capabilities`;
|
|
74
|
+
- an unknown required capability fails validation;
|
|
75
|
+
- an unknown optional capability may be ignored, but it does not authorize
|
|
76
|
+
unknown fields, markup, media, or files—the closed v1 schema still applies;
|
|
77
|
+
- `taproot.docs.fragments.html.v1` is required by every v1 artifact.
|
|
78
|
+
|
|
79
|
+
The npm package uses semantic versioning independently of `schemaVersion`:
|
|
80
|
+
|
|
81
|
+
- a package major changes the JavaScript API, compatibility policy, or supported
|
|
82
|
+
manifest major;
|
|
83
|
+
- a package minor adds a backward-compatible validator entry point, understood
|
|
84
|
+
optional capability, or conformance case for an already supported manifest
|
|
85
|
+
major;
|
|
86
|
+
- a package patch fixes implementation, diagnostics, or documented security
|
|
87
|
+
enforcement. A patch may reject an input that never conformed to the stated
|
|
88
|
+
fail-closed rules.
|
|
89
|
+
|
|
90
|
+
Producer and consumer release PRs update one exact dependency pin together. The
|
|
91
|
+
private Taproot `docs-artifact-v<package-version>` tag does not publish npm
|
|
92
|
+
directly. It verifies and copies the reviewed package allowlist into
|
|
93
|
+
`packages/docs-artifact/` in public
|
|
94
|
+
[`taprootio/trunk`](https://github.com/taprootio/trunk), commits it, and creates
|
|
95
|
+
the matching public tag. Trunk's `publish-docs-artifact.yml` then requires that
|
|
96
|
+
tag to equal `package.json` and belong to public `main` before publishing from a
|
|
97
|
+
GitHub-hosted runner. npm provenance therefore resolves to the exact public
|
|
98
|
+
Trunk release rather than to private Taproot.
|
|
99
|
+
|
|
100
|
+
The public publish job binds the `npm-docs-artifact-publish` GitHub Environment.
|
|
101
|
+
The first registry publish is `1.0.1` and uses a one-time granular `NPM_TOKEN`
|
|
102
|
+
because npm cannot configure a trusted publisher before a package exists. The
|
|
103
|
+
immutable `1.0.0` Trunk tag records a bootstrap attempt that failed in package
|
|
104
|
+
tests before npm publish, so that version is intentionally absent from the
|
|
105
|
+
registry. Administrators then
|
|
106
|
+
configure the trusted publisher for `taprootio/trunk`, workflow
|
|
107
|
+
`publish-docs-artifact.yml`, and that Environment, and delete the bootstrap
|
|
108
|
+
token. Steady-state releases use short-lived OIDC only. The complete setup and
|
|
109
|
+
recovery procedure lives in Trunk's `RELEASING.md`; the private scaffold is
|
|
110
|
+
maintained under `release/trunk/public-repo/`.
|
|
111
|
+
|
|
112
|
+
## Determinism
|
|
113
|
+
|
|
114
|
+
`serializeManifest()` validates first, recursively sorts object keys, emits two-
|
|
115
|
+
space JSON, and adds one final newline. Producers also must use these deterministic
|
|
116
|
+
array orders, which validation enforces:
|
|
117
|
+
|
|
118
|
+
- capabilities, locales, resources, resource locale variants, semantic audience
|
|
119
|
+
and tag lists, navigation locale sets, redirects, and assets are sorted by
|
|
120
|
+
JavaScript UTF-16 code-unit order (the locale-independent ordering used by
|
|
121
|
+
string relational comparison and `Array.prototype.sort()` without a
|
|
122
|
+
comparator);
|
|
123
|
+
- headings, fragments, and navigation nodes preserve their semantic document
|
|
124
|
+
order.
|
|
125
|
+
|
|
126
|
+
Paths are NFC-normalized, lowercase, relative POSIX paths. Percent escapes,
|
|
127
|
+
backslashes, controls, leading slashes, empty segments, and dot segments are
|
|
128
|
+
rejected rather than normalized silently. Every managed path segment also
|
|
129
|
+
rejects Windows device aliases (`con`, `prn`, `aux`, `nul`, `com1`–`com9`, and
|
|
130
|
+
`lpt1`–`lpt9`), including aliases followed by a file extension. The same
|
|
131
|
+
portable device-name rule applies to every route segment. Routes are lowercase
|
|
132
|
+
root-relative directory URLs with one trailing slash. Taproot shell path
|
|
133
|
+
prefixes including `/.well-known/`, `/_taproot/`, `/api/`, `/assets/`,
|
|
134
|
+
`/bust/`, `/pagefind/`, `/public/`, and `/taproot-docs/` are reserved. The
|
|
135
|
+
exact `/404/` route is also reserved. Root file routes
|
|
136
|
+
(`/404.html/`, `/favicon.ico/`, `/index.html/`, `/manifest.webmanifest/`,
|
|
137
|
+
`/robots.txt/`, `/sitemap.xml/`, and
|
|
138
|
+
`/taproot-docs-manifest.json/`) reserve their complete first-segment subtree so
|
|
139
|
+
a route cannot turn a published root file into a directory.
|
|
140
|
+
|
|
141
|
+
Manifest string bounds count Unicode scalar values, matching JSON Schema
|
|
142
|
+
`minLength` and `maxLength`; astral characters count once rather than as two
|
|
143
|
+
UTF-16 code units. The 2 MiB manifest ceiling applies both to supplied JSON bytes
|
|
144
|
+
and to the canonical UTF-8 serialization of an object input. A manifest that
|
|
145
|
+
cannot be serialized within that bound is not v1-conforming. Raw string input is
|
|
146
|
+
rejected first by a conservative UTF-16 length bound, then by an exact bounded
|
|
147
|
+
UTF-8 count, before the validator allocates its encoded bytes. Object input is
|
|
148
|
+
preflighted with cycle, depth, and traversal-work bounds; repeated references
|
|
149
|
+
count every serialized occurrence without retraversing the shared object. That
|
|
150
|
+
same traversal reads each own JSON property once and materializes a
|
|
151
|
+
null-prototype snapshot. Schema validation, canonical measurement,
|
|
152
|
+
serialization, artifact validation, and returned values use only that snapshot,
|
|
153
|
+
so accessors and Proxies cannot change a manifest between validation stages.
|
|
154
|
+
Uint8Array manifests and binary file entries are measured and copied through
|
|
155
|
+
their built-in internal slots; shadowed `byteLength` properties and caller
|
|
156
|
+
iterators are never treated as byte authority. ArrayBuffer and Uint8Array byte
|
|
157
|
+
lengths are checked against the manifest ceiling before a private snapshot is
|
|
158
|
+
allocated. Genuine Uint8Array and ArrayBuffer values from another JavaScript
|
|
159
|
+
realm are accepted; proxies, `Symbol.toStringTag` lookalikes, detached buffers,
|
|
160
|
+
and shared buffers fail closed.
|
|
161
|
+
|
|
162
|
+
`build.producerVersion` is SemVer 2.0. Numeric prerelease identifiers do not
|
|
163
|
+
permit leading zeroes (`1.0.0-0` and `1.0.0-alpha.1` are valid;
|
|
164
|
+
`1.0.0-01` is not). Numeric build identifiers may retain leading zeroes.
|
|
165
|
+
|
|
166
|
+
Duplicate resource keys, locale variants, routes, redirect sources, asset keys,
|
|
167
|
+
heading ids, fragment keys, declared file paths, navigation references, JSON
|
|
168
|
+
object keys, and supplied file entries fail deterministically. Diagnostics are
|
|
169
|
+
sorted by path, code, and message using locale-independent string ordering.
|
|
170
|
+
Diagnostic paths and messages are NFC-normalized, escape C0/C1 and bidirectional
|
|
171
|
+
formatting controls, and are bounded by exported scalar and UTF-8 byte ceilings
|
|
172
|
+
before they are returned or included in a thrown validation error.
|
|
173
|
+
|
|
174
|
+
All manifest string values and all decoded markup attribute values reject C0,
|
|
175
|
+
C1, and bidirectional formatting/override controls. These checks use the same
|
|
176
|
+
pinned code-point ranges in both validators, including when an allowed HTML
|
|
177
|
+
entity decodes into the inspected attribute value.
|
|
178
|
+
|
|
179
|
+
Docs v1 also pins a deterministic, registry-independent BCP 47 subset instead
|
|
180
|
+
of asking the host's ICU database to decide locale validity. A supported tag
|
|
181
|
+
contains a lowercase two- or three-letter language, an optional title-case
|
|
182
|
+
four-letter Script subtag, and an optional uppercase two-letter or three-digit
|
|
183
|
+
region subtag: for example `en`, `en-US`, `fr-FR`, `zh-Hant`, `es-419`, or
|
|
184
|
+
`abc-Latn-419`. Variants, extensions, private-use tags, grandfathered tags, and
|
|
185
|
+
other casing are outside v1. The JSON Schema uses this same grammar and length
|
|
186
|
+
ceiling as the runtime manifest and markup `lang` validators.
|
|
187
|
+
|
|
188
|
+
## Content and resource bounds
|
|
189
|
+
|
|
190
|
+
The exported `LIMITS` object is the v1 ceiling. Important bounds are:
|
|
191
|
+
|
|
192
|
+
| Input | Maximum |
|
|
193
|
+
|---|---:|
|
|
194
|
+
| Manifest | 2 MiB |
|
|
195
|
+
| Object-manifest traversal / nesting depth | 250,000 values / 64 |
|
|
196
|
+
| Managed semantic bytes | 256 MiB |
|
|
197
|
+
| Listed semantic files | 20,000 |
|
|
198
|
+
| Managed filesystem entries | 40,000 |
|
|
199
|
+
| Managed directory depth | 32 |
|
|
200
|
+
| Managed relative path | 512 characters |
|
|
201
|
+
| Resources | 10,000 |
|
|
202
|
+
| Locale variants | 20,000 |
|
|
203
|
+
| Fragments | 20,000 |
|
|
204
|
+
| Assets | 10,000 |
|
|
205
|
+
| Redirects | 10,000 |
|
|
206
|
+
| Navigation nodes / depth | 20,000 / 12 |
|
|
207
|
+
| Markup elements / nesting depth per fragment | 100,000 / 128 |
|
|
208
|
+
| One fragment | 2 MiB |
|
|
209
|
+
| One asset | 25 MiB |
|
|
210
|
+
| Decoded image canvas | 67,108,864 pixels |
|
|
211
|
+
| Cumulative decoded animated frames | 67,108,864 pixels |
|
|
212
|
+
| Animated image frames | 1,000 |
|
|
213
|
+
| Consumer-supported capabilities | 100 entries, 200 characters each |
|
|
214
|
+
|
|
215
|
+
Navigation depth counts each top-level item as level one. A leaf at level 12 is
|
|
216
|
+
valid; a child node at level 13 is rejected, while the global node ceiling still
|
|
217
|
+
counts nodes across every locale and nested array.
|
|
218
|
+
|
|
219
|
+
In-memory artifact validation charges every inspected declared entry's actual
|
|
220
|
+
UTF-8 or binary length to the aggregate budget before per-file size-drift
|
|
221
|
+
handling. String content is checked for well-formed UTF-16 while its UTF-8 bytes
|
|
222
|
+
are counted in the same bounded pass; scanning stops as soon as the remaining
|
|
223
|
+
aggregate budget is exceeded. File iteration and all later hashing or media
|
|
224
|
+
parsing stop at that point.
|
|
225
|
+
|
|
226
|
+
Consumers may impose smaller product or license limits but may not reinterpret a
|
|
227
|
+
larger input as v1-conforming. Archive compression expansion, entry type, and
|
|
228
|
+
physical-storage quotas remain additional ingestion-boundary checks; the Node
|
|
229
|
+
directory validator already rejects symbolic links and non-regular entries
|
|
230
|
+
anywhere below the managed subtree. It collects only the remaining entry budget
|
|
231
|
+
plus one, then walks entries in canonical name order; an over-budget tree returns
|
|
232
|
+
only its deterministic limit diagnostic. Managed files are opened once in
|
|
233
|
+
nonblocking mode without following a final symbolic link, checked by `stat`
|
|
234
|
+
against the descriptor's exact declared byte count before allocation, and read
|
|
235
|
+
through that handle under one cumulative 256 MiB budget. Nonblocking opens make
|
|
236
|
+
an enumerated-file-to-FIFO race fail type validation instead of waiting for a
|
|
237
|
+
writer. Files are rejected if their identity or size changes during the read.
|
|
238
|
+
After each read, the validator re-lstats every path component,
|
|
239
|
+
re-resolves the final path beneath the artifact root's original real path, and
|
|
240
|
+
requires the resolved regular file to retain the opened handle's device and
|
|
241
|
+
inode. Replacing either the file or one of its ancestors with a symbolic link is
|
|
242
|
+
therefore rejected even if it races the handle read. Before success, the same
|
|
243
|
+
bounded managed-subtree enumeration is repeated and its relative path/type set
|
|
244
|
+
must match the original walk.
|
|
245
|
+
|
|
246
|
+
The Node directory API snapshots `supportedCapabilities` once, with the
|
|
247
|
+
exported entry ceiling, before reading the artifact. Arrays, sets, and one-shot
|
|
248
|
+
generator objects are safe to pass and are not revisited by the manifest and
|
|
249
|
+
artifact validation phases; a scalar string is not a capability collection.
|
|
250
|
+
Every supplied value must be a primitive canonical capability/resource-key
|
|
251
|
+
string no longer than 200 Unicode scalar values. Invalid collections, values,
|
|
252
|
+
and iterator failures produce bounded generic diagnostics.
|
|
253
|
+
|
|
254
|
+
## Supported semantic HTML
|
|
255
|
+
|
|
256
|
+
Fragments are exact `text/html; charset=utf-8` bytes and use a small semantic
|
|
257
|
+
allow-list: headings `h2`–`h6`, paragraphs, lists, tables, figures, details,
|
|
258
|
+
code/preformatted text, common inline semantics, links, and images. The validator
|
|
259
|
+
rejects comments, declarations, custom elements, forms, scripts, styles, iframes,
|
|
260
|
+
event attributes, inline styles, classes, unquoted attributes, malformed nesting,
|
|
261
|
+
browser-reparenting content models, raw whitespace before tag names, and unknown
|
|
262
|
+
markup. HTML syntax recognizes only tab, line feed, form feed, carriage return,
|
|
263
|
+
and space as whitespace; other Unicode whitespace remains content or is rejected
|
|
264
|
+
where browsers do not treat it as syntax whitespace.
|
|
265
|
+
|
|
266
|
+
Entities are deliberately narrower than the full browser named-entity table.
|
|
267
|
+
Only semicolon-terminated `amp`, `apos`, `colon`, `gt`, `lt`, and `quot` named
|
|
268
|
+
references plus scalar numeric references are accepted. Raw or semicolonless
|
|
269
|
+
ampersands, unknown names, and numeric references that browsers replace or map
|
|
270
|
+
through control-character rules fail closed. Literal U+0000 text fails with
|
|
271
|
+
`markup.invalid_null`, matching the fail-closed handling of nulls in attributes
|
|
272
|
+
and numeric references. Element count and open-element
|
|
273
|
+
nesting stop immediately at the exported v1 ceilings above.
|
|
274
|
+
|
|
275
|
+
Internal links use `data-resource-key` plus optional `data-heading-id`; same-page
|
|
276
|
+
links may use `href="#heading-id"`; external links are credential-free HTTPS.
|
|
277
|
+
Images use `data-asset-key` and required `alt` text rather than `src`. Only GIF,
|
|
278
|
+
JPEG, PNG, and WebP assets are accepted, and bytes must match the declared
|
|
279
|
+
complete container structure, dimensions, size, and SHA-256. Validation walks
|
|
280
|
+
bounded container records and framing but does not decode pixels. GIF, JPEG,
|
|
281
|
+
PNG/APNG, and WebP canvas headers must remain within the decoded-pixel ceiling;
|
|
282
|
+
declared asset width multiplied by height is subject to the same runtime
|
|
283
|
+
ceiling. JSON Schema mirrors each dimension's maximum and documents this
|
|
284
|
+
cross-field product, which JSON Schema cannot express exactly. GIF, APNG, and
|
|
285
|
+
animated WebP frame records must remain within the animation-frame ceiling.
|
|
286
|
+
The sum of decoded frame rectangles in one GIF, APNG, or animated WebP must
|
|
287
|
+
also remain within the cumulative decoded-animation-pixel ceiling.
|
|
288
|
+
GIF frames require a valid global or local color table. Graphic Control,
|
|
289
|
+
Application, Plain Text, and Comment extensions follow their label-specific
|
|
290
|
+
block grammar; unknown extension labels and reserved packed-field bits fail
|
|
291
|
+
closed. PNG chunk types contain only ASCII letters with an uppercase reserved
|
|
292
|
+
third byte, and every IDAT chunk (including an empty one) belongs to one
|
|
293
|
+
contiguous run. PNG palette presence, entry count, and placement follow the
|
|
294
|
+
IHDR color type and bit depth. APNG frame controls and data use one gapless
|
|
295
|
+
sequence, valid dispose/blend operations, correct IDAT/fdAT ownership, and a
|
|
296
|
+
completed-frame count matching acTL. JPEG assets are complete baseline Huffman
|
|
297
|
+
interchange streams from SOI through EOI: referenced quantization and Huffman
|
|
298
|
+
tables, frame and scan component declarations, and entropy/restart framing are
|
|
299
|
+
validated. A referenced quantization table may appear before or after SOF0 but
|
|
300
|
+
must be defined before its component's SOS is accepted. Complete baseline
|
|
301
|
+
sequential images may use one interleaved scan or multiple scans; every frame
|
|
302
|
+
component must appear exactly once across them. DQT, DHT, DRI, COM, and APP
|
|
303
|
+
metadata may appear between scans, and each scan independently validates table
|
|
304
|
+
references, sequential spectral fields, restart cardinality, and entropy
|
|
305
|
+
framing. Baseline quantization tables contain 64 nonzero 8-bit coefficients;
|
|
306
|
+
Huffman tables have at most 256 baseline-valid symbols and never exhaust prefix
|
|
307
|
+
space, leaving the forbidden all-ones terminal code unused. Component sampling
|
|
308
|
+
products total at most 10. Entropy byte stuffing is exactly `FF 00`; repeated
|
|
309
|
+
`FF` bytes are fill only before a nonzero marker. A DRI scan contains exactly
|
|
310
|
+
`floor((MCUs - 1) / interval)` ordered restart markers, and every marker
|
|
311
|
+
separates nonempty entropy partitions. Progressive, arithmetic-coded, and
|
|
312
|
+
otherwise non-baseline streams fail closed.
|
|
313
|
+
Animated WebP frame rectangles use the format's doubled x/y units, must stay
|
|
314
|
+
within the VP8X canvas, must leave reserved flag bits clear, and must match the
|
|
315
|
+
dimensions of their embedded VP8 or VP8L image. WebP chunks with odd payload
|
|
316
|
+
lengths require a present zero pad byte. VP8X is first and unique for extended
|
|
317
|
+
files. Its ICC, alpha, Exif, XMP, and animation feature bits must exactly match
|
|
318
|
+
the chunks and decoded VP8L alpha-used headers observed in the container. The
|
|
319
|
+
canonical extended order is VP8X, optional unique ICCP, one still reconstruction
|
|
320
|
+
(ALPH immediately followed by VP8, or VP8L) or one animation reconstruction
|
|
321
|
+
(ANIM followed by a contiguous ANMF run), then optional unique EXIF and XMP
|
|
322
|
+
chunks in that order. Still and animated reconstruction are exclusive. An ANMF
|
|
323
|
+
payload is exactly one VP8L chunk or an optional ALPH followed by one VP8 chunk;
|
|
324
|
+
nested alpha observations are accumulated across all frames. VP8 keyframe tags
|
|
325
|
+
must describe a displayed, non-experimental frame with a bounded nonempty first
|
|
326
|
+
partition; VP8L requires bytes beyond its five-byte header. ALPH headers accept
|
|
327
|
+
only defined compression, filter, and preprocessing fields, and uncompressed
|
|
328
|
+
alpha must contain exactly one byte per frame pixel. Unrecognized top-level and
|
|
329
|
+
nested WebP chunks fail closed. These bounds fail before any consumer decodes
|
|
330
|
+
image payloads. SVG, HTML, CSS, JavaScript, fonts, and arbitrary downloads are
|
|
331
|
+
not managed Docs v1 assets.
|
|
332
|
+
|
|
333
|
+
## API
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
import {
|
|
337
|
+
assertValidArtifact,
|
|
338
|
+
serializeManifest,
|
|
339
|
+
validateArtifact,
|
|
340
|
+
validateManifest,
|
|
341
|
+
} from "@taprootio/docs-artifact";
|
|
342
|
+
import { loadConformanceCases } from "@taprootio/docs-artifact/conformance";
|
|
343
|
+
import { validateArtifactDirectory } from "@taprootio/docs-artifact/node";
|
|
344
|
+
|
|
345
|
+
const manifestResult = validateManifest(manifestBytes);
|
|
346
|
+
const artifactResult = await validateArtifact(manifestBytes, semanticFiles);
|
|
347
|
+
const directoryResult = await validateArtifactDirectory("_site");
|
|
348
|
+
const canonicalBytes = serializeManifest(manifestObject);
|
|
349
|
+
const sharedCases = await loadConformanceCases();
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The assertion variants throw `DocsArtifactValidationError` with the same stable
|
|
353
|
+
`errors` array. The CLI prints `code path: message` diagnostics and exits nonzero:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
npx --package=@taprootio/docs-artifact@1.0.1 taproot-docs-validate ./_site
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Consumers should assert error `code` and `path`, not human-readable wording.
|
|
360
|
+
The package test suite also runs `npm pack --dry-run --json --ignore-scripts`
|
|
361
|
+
and requires the exact reviewed 36-file tarball inventory, including the ISC
|
|
362
|
+
`LICENSE`, so a publishable file cannot appear or disappear silently.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { validateArtifact } from "../src/artifact-validator.js";
|
|
4
|
+
import { loadConformanceCases } from "../src/conformance.js";
|
|
5
|
+
|
|
6
|
+
let failed = false;
|
|
7
|
+
for (const fixture of await loadConformanceCases()) {
|
|
8
|
+
const result = await validateArtifact(fixture.manifest, fixture.files);
|
|
9
|
+
const codes = result.ok ? [] : [...new Set(result.errors.map((error) => error.code))].sort();
|
|
10
|
+
const expected = [...fixture.expectedCodes].sort();
|
|
11
|
+
if (result.ok !== fixture.valid || JSON.stringify(codes) !== JSON.stringify(expected)) {
|
|
12
|
+
failed = true;
|
|
13
|
+
process.stderr.write(`${fixture.name}: expected ${JSON.stringify(expected)}, received ${JSON.stringify(codes)}\n`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (failed) {
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
} else {
|
|
19
|
+
process.stdout.write("Taproot Docs artifact conformance fixtures passed.\n");
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { validateArtifactDirectory } from "../src/node.js";
|
|
4
|
+
|
|
5
|
+
const arguments_ = process.argv.slice(2);
|
|
6
|
+
const json = arguments_.includes("--json");
|
|
7
|
+
const directory = arguments_.find((argument) => argument !== "--json") ?? process.cwd();
|
|
8
|
+
const result = await validateArtifactDirectory(directory);
|
|
9
|
+
|
|
10
|
+
if (json) {
|
|
11
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
12
|
+
} else if (result.ok) {
|
|
13
|
+
process.stdout.write(`Valid Taproot Docs artifact: ${result.value.fileCount} semantic files, ${result.value.totalBytes} bytes.\n`);
|
|
14
|
+
} else {
|
|
15
|
+
for (const error of result.errors) process.stderr.write(`${error.code} ${error.path}: ${error.message}\n`);
|
|
16
|
+
}
|
|
17
|
+
process.exitCode = result.ok ? 0 : 1;
|
package/conformance.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ArtifactFileEntry } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export interface DocsArtifactConformanceCase {
|
|
4
|
+
name: string;
|
|
5
|
+
valid: boolean;
|
|
6
|
+
expectedCodes: string[];
|
|
7
|
+
manifest: Uint8Array;
|
|
8
|
+
files: ArtifactFileEntry[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function loadConformanceCases(): Promise<DocsArtifactConformanceCase[]>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Conformance fixtures
|
|
2
|
+
|
|
3
|
+
Consume these cases through `loadConformanceCases()` from
|
|
4
|
+
`@taprootio/docs-artifact/conformance`. It returns manifest bytes and exact
|
|
5
|
+
`{ path, content }` entries for each case, including decoded binary assets and
|
|
6
|
+
manifest mutations used to isolate one invalid behavior.
|
|
7
|
+
|
|
8
|
+
Large adversarial markup and navigation-depth boundaries are represented
|
|
9
|
+
compactly in `conformance.json` with bounded generation recipes. The loader
|
|
10
|
+
materializes generated bytes and synchronizes the named descriptor's size and
|
|
11
|
+
SHA-256 before returning the case. Oversized-manifest cases similarly use
|
|
12
|
+
deterministic trailing-space padding, keeping reviewable fixtures small without
|
|
13
|
+
weakening the public case.
|
|
14
|
+
|
|
15
|
+
The published cases also pin accepted and rejected boundaries for the
|
|
16
|
+
registry-independent Docs v1 locale subset and reject C1 or bidirectional
|
|
17
|
+
formatting/override controls in manifest strings. These cases keep producers,
|
|
18
|
+
consumers, the runtime validator, and the JSON Schema on one deterministic text
|
|
19
|
+
contract regardless of host ICU data.
|
|
20
|
+
|
|
21
|
+
`valid/minimal/` is also a directly valid directory fixture. The complete
|
|
22
|
+
localized fixture keeps its one PNG as reviewable base64 in source control; the
|
|
23
|
+
loader materializes it at the manifest path without requiring a generated binary
|
|
24
|
+
file in the contract repository.
|