@textmode/document 1.0.0 → 1.1.0

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.
@@ -1,19 +1,19 @@
1
1
  # Textmode document format 2.0
2
2
 
3
- ## Status
3
+ > Versioned v2 reference. Current v3 documentation is in the
4
+ > [v3 specification](specification-3.0.md) and [visual guide](schema-3.0.md).
5
+ > The current decoder validates these older shapes and migrates them to v3.
4
6
 
5
- This document defines `textmode.document` version `2.0.0`. The canonical
6
- specification, schema, and decoder are maintained by `textmode-document` and
7
- published as `@textmode/document`. The raw schema is available from
8
- `@textmode/document/schema/2.0` and [the repository](../schemas/2.0/schema.json).
9
- The schema's `$id` is a stable identifier, not a hosted download URL.
7
+ ## Status
10
8
 
11
- The ESM-only package root exposes the decoder and types. Package version `1.0.0`
12
- is independent of file version `2.0.0`. Test fixtures are private repository data;
13
- complete public documents appear in the [consumer guide](consumer-guide.md).
9
+ This specification defines `textmode.document@2.0.0`. Its [JSON Schema](../schemas/2.0/schema.json)
10
+ is also available as `@textmode/document/schema/2.0`. The schema's `$id` is an
11
+ identifier and does not guarantee a hosted download. Package and document versions
12
+ are independent.
14
13
 
15
- The format represents editable cell data. It does not represent final rendered
16
- pixels, post-processing, an editor project, or authored animation.
14
+ Version 2 stores editable cell data. It does not store final rendered pixels,
15
+ post-processing, an editor project, or authored animation. For complete examples
16
+ of the current format, see the [consumer guide](consumer-guide.md).
17
17
 
18
18
  ## Encoding
19
19
 
@@ -25,9 +25,8 @@ profile in RFC 7493:
25
25
  - non-finite numbers are not emitted;
26
26
  - timestamps use an uppercase, timezone-qualified RFC 3339 representation.
27
27
 
28
- These are producer requirements and recommendations. The JavaScript parser uses
29
- native `JSON.parse`, which does not detect duplicate object keys and retains the
30
- last value. No duplicate-key detection is claimed by this codec.
28
+ The JavaScript parser uses `JSON.parse`, which keeps the last value for a
29
+ repeated key. It does not report duplicate keys.
31
30
 
32
31
  The media type is `application/json`. `.textmode.json` is a suggested filename
33
32
  suffix; existing exporter downloads use `.json` by default. Extensions and
@@ -57,16 +56,15 @@ container format.
57
56
 
58
57
  ## Version compatibility
59
58
 
60
- The version is SemVer-shaped for release communication. Version 2 does not
61
- define reader negotiation or a formal extension mechanism. Consumers therefore
62
- accept only explicitly tested versions and migrations; they must not assume an
63
- unknown `2.x` version is compatible.
59
+ The version uses a SemVer-shaped identifier. Version 2 does not define reader
60
+ negotiation or a formal extension mechanism. Consumers must accept only explicitly
61
+ tested versions and migrations; an unknown `2.x` version is not automatically
62
+ compatible.
64
63
 
65
- For a supported version, consumers ignore unknown ordinary object members.
66
- The canonical decoder omits these members from its new output object; it does
67
- not preserve them for reserialization.
68
- They reject unknown values of tagged members such as `format`, `target`, and
69
- `encoding` because those values change interpretation.
64
+ For a supported version, consumers ignore unknown ordinary fields. The decoder
65
+ omits them from canonical output, so reserialization does not preserve them.
66
+ Consumers reject unknown `format`, `target`, and `encoding` values because those
67
+ values change how to read the document.
70
68
 
71
69
  Version 2 does not define `extensionsUsed`, `extensionsRequired`, extension
72
70
  namespacing, or an extension registry. A later revision must define those
@@ -128,8 +126,7 @@ canvas.height = grid.rows × grid.cellHeight
128
126
  For an `all` document, the root extent is derived from the first layer's grid.
129
127
  Consumers must not infer composite placement or clipping from this field.
130
128
 
131
- A future major format may replace this legacy name with `gridPixelSize`; v2
132
- semantics must not be changed in place.
129
+ The meaning of this field must not change within version 2.
133
130
 
134
131
  ## Cell encoding
135
132
 
@@ -231,10 +228,9 @@ throws `TypeError`. Invalid document data instead returns a typed failure.
231
228
  | `maxCharacterLength` | 64 | Unicode scalar values per glyph string. |
232
229
  | `maxMetadataStringLength` | 4,096 | UTF-16 code units per metadata string and blend-mode name. |
233
230
 
234
- These budgets apply after JSON parsing and do not bound file bytes or parser
235
- memory. The schema's fixed glyph `maxLength` remains 64 even when an app raises
236
- the decoder's glyph budget. Product-specific policy belongs to the product. For example,
237
- `create.textmode.art` may accept fewer cells than another document consumer.
231
+ These budgets apply after JSON parsing and do not limit file bytes or parser
232
+ memory. The schema's glyph `maxLength` remains 64 even when an app raises the
233
+ decoder limit. Applications can also enforce their own feature and size limits.
238
234
 
239
235
  Consumers should:
240
236
 
@@ -254,14 +250,14 @@ The compatibility decoder recognizes:
254
250
  - `textmode.layer@1.0.0` as a selected cell snapshot;
255
251
  - `textmode.layer@1.1.0` as a layer stack.
256
252
 
257
- Both migrate to the canonical v2 union and return a migration warning. Callers
258
- may disable legacy decoding.
253
+ The current decoder migrates both to canonical v3 output and returns
254
+ `LEGACY_DOCUMENT_MIGRATED`. Set `acceptLegacy: false` to reject these legacy files;
255
+ this does not disable v2 document support.
259
256
 
260
257
  ## JSON Schema and runtime decoder
261
258
 
262
- The specification defines meaning and cross-field rules. The JSON Schema describes the portable structure and is suitable for
263
- documentation and general tooling. The runtime decoder remains authoritative
264
- for:
259
+ The specification defines meaning and cross-field rules. The JSON Schema describes
260
+ structure for general tooling. The decoder also checks or applies:
265
261
 
266
262
  - cross-field row and grid invariants;
267
263
  - grid pixel extent equality;
@@ -0,0 +1,134 @@
1
+ # Textmode document format 3.0
2
+
3
+ ## Status and scope
4
+
5
+ This specification defines `textmode.document@3.0.0`. The npm package version
6
+ is independent of the document version. The [JSON Schema](../schemas/3.0/schema.json)
7
+ uses Draft 2020-12 and is available as `@textmode/document/schema/3.0`. Its `$id`
8
+ is an identifier and does not guarantee a hosted download.
9
+
10
+ A document stores editable cells in ordered layers. It does not store font identity
11
+ or assets, a viewport, animation, editor project state, history, or post-processing.
12
+
13
+ ## Encoding and root
14
+
15
+ Files are UTF-8 JSON objects. Producers should use unique keys, safe integers,
16
+ finite numbers, and uppercase, timezone-qualified timestamps. JavaScript's
17
+ `JSON.parse` keeps the last value when a key is repeated; it does not report
18
+ duplicates. The `.textmode.json` suffix and `application/json` MIME type are hints.
19
+ Readers identify the format using `format` and `formatVersion`.
20
+
21
+ The root requires `format: "textmode.document"`, `formatVersion: "3.0.0"`, and
22
+ `layers`. Optional `metadata` is defined below. `layers` is a nonempty array in
23
+ bottom-to-top order; there is no separate single-layer shape. Root `target`,
24
+ `layer`, `grid`, and `canvas` are obsolete and invalid, including when null.
25
+
26
+ Unknown ordinary fields are allowed, except for the reserved root fields above.
27
+ The decoder omits unknown fields from its canonical output, so decoding and
28
+ reserializing does not preserve extensions. Readers reject unknown `format`,
29
+ `formatVersion`, and cell `encoding` values. Accept only explicitly supported
30
+ versions: v3 does not define version negotiation, required extensions, an
31
+ extension registry, or automatic compatibility with future 3.x versions.
32
+
33
+ ## Layer structure and presentation
34
+
35
+ Every layer requires all fields below, including when it is the only layer:
36
+
37
+ | Field | Meaning |
38
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39
+ | `id` | Nonempty informational source ID; not a cross-file identity. Uniqueness is not enforced. |
40
+ | `visible` | Boolean; hidden cells remain stored. |
41
+ | `opacity` | Number in [0, 1], independent of cell alpha. |
42
+ | `blendMode` | Nonempty bounded name; unknown names are preserved. Apps choose which names they support. |
43
+ | `offsetX`, `offsetY` | Finite pixel offsets; positive x is right and positive y is down in textmode.js. |
44
+ | `rotationZ` | Finite degrees, clockwise about the layer's rectangular center in textmode.js. Offset does not change the local rotation origin. Preserved without normalization. |
45
+ | `grid` | Independent logical dimensions and cell pixel metrics. |
46
+ | `cells` | Complete dense row-major cell data. |
47
+
48
+ The textmode.js producer names are `normal`, `additive`, `multiply`, `screen`,
49
+ `subtract`, `darken`, `lighten`, `overlay`, `softLight`, `hardLight`,
50
+ `colorDodge`, `colorBurn`, `difference`, and `exclusion`. These are not an enum
51
+ in the portable schema. Apps must not silently replace an unsupported meaningful
52
+ mode with `normal`.
53
+
54
+ Each `grid` requires positive safe integers for `cols`, `rows`, `cellWidth`, and
55
+ `cellHeight`. The cell count and pixel products must also be safe integers. Layers
56
+ can have different grids. A layer's pixel extent is `cols × cellWidth` by
57
+ `rows × cellHeight`. Do not infer a root grid or viewport.
58
+
59
+ ## Cells and coordinates
60
+
61
+ Every `cells` object requires `encoding: "object-rows-v1"` and `rows`.
62
+ `rows.length` equals `grid.rows`; each row length equals `grid.cols`. Every cell,
63
+ including transparent cells, is present. A cell requires `x`, `y`, `character`,
64
+ `foreground`, `background`, and `transform`.
65
+
66
+ Coordinates are zero-based nonnegative safe integers. In `rows[y][x]`, the
67
+ cell's stored `x` and `y` must match its array position. Consumers must reject a
68
+ mismatch without repairing or reordering cells. The decoder enforces this rule.
69
+
70
+ ## Glyphs, colors, and transforms
71
+
72
+ `character` is a nonempty, well-formed Unicode glyph string. It may contain
73
+ multiple scalar values, such as a combining sequence or joined emoji. Preserve it
74
+ without Unicode normalization. Unpaired surrogates are invalid. The schema uses
75
+ `minLength: 1` and `maxLength: 64`; the default decoder limit is 64 Unicode scalar
76
+ values. Changing a decoder limit does not change the published schema.
77
+
78
+ Each color is `#RRGGBB`, `#RRGGBBAA`, or an object with required integer `r`, `g`,
79
+ `b`, and `a` channels in [0, 255]. Six-digit notation implies alpha 255. Channels
80
+ are unpremultiplied; alpha 0 is transparent and 255 is opaque. Canonical output
81
+ uses RGBA channel objects. A space with visible background remains meaningful;
82
+ a cell is visually empty only when both foreground and background alpha are zero.
83
+
84
+ Each transform requires boolean `invert`, `flipX`, and `flipY` values and a finite
85
+ `rotation` in degrees. The decoder normalizes cell rotation to [0, 360): -90
86
+ becomes 270 and 360 becomes 0. Layer rotation is separate. Swapping colors is
87
+ not a general replacement for inversion when alpha is present. Consumers must
88
+ reject unsupported meaningful transforms rather than approximate them without an
89
+ explicit product operation.
90
+
91
+ ## Metadata
92
+
93
+ Optional root `metadata`, when present, requires `createdAt` and `generator`.
94
+ `generator` requires nonempty bounded `name` and `version` strings and can
95
+ identify any producer. It does not select executable behavior.
96
+
97
+ `createdAt` is a real calendar date and time with uppercase T, required seconds,
98
+ a timezone, optional fractional seconds, and no leap seconds. Valid timestamps
99
+ are preserved as written. Enable date-time format assertions in JSON Schema
100
+ validators; format annotations alone are insufficient.
101
+
102
+ ## Validation, budgets, and compatibility
103
+
104
+ The schema defines structure and local constraints. This specification defines
105
+ meaning. The decoder also checks row sizes, coordinates, safe products, Unicode,
106
+ and caller limits, and normalizes output. A schema-valid document may still fail
107
+ decoding.
108
+
109
+ | Decoder limit | Default | Counting rule |
110
+ | ------------------------- | ---------: | ---------------------------------------------------------- |
111
+ | `maxCells` | 16,777,216 | Cumulative across all layers. |
112
+ | `maxLayers` | 4,096 | Layer count. |
113
+ | `maxLayerIdLength` | 1,024 | UTF-16 code units per ID. |
114
+ | `maxCharacterLength` | 64 | Unicode scalar values per glyph. |
115
+ | `maxMetadataStringLength` | 4,096 | UTF-16 code units per metadata string and blend-mode name. |
116
+
117
+ Every override must be a positive safe integer. Invalid configuration throws
118
+ `TypeError`; invalid document input returns a failure result. Budgets apply
119
+ after parsing and do not bound input bytes or parser memory. Apps must apply
120
+ appropriate byte caps before reading/parsing and their own font and feature
121
+ policy after protocol decoding. Documents do not authorize fetching resources,
122
+ font installation, or code execution.
123
+
124
+ Supported v2 files are validated under their original rules, including canvas
125
+ extent checks, before conversion. Selected v2 snapshots move their root grid
126
+ into one layer and receive visible=true, opacity=1, normal blending, and zero
127
+ offsets/rotation. V2 stacks preserve their layers. Both return
128
+ `DOCUMENT_VERSION_MIGRATED` and canonical v3 output.
129
+
130
+ Legacy `textmode.layer@1.0.0` and `textmode.layer@1.1.0` migrate to one or more
131
+ layers and return `LEGACY_DOCUMENT_MIGRATED`. `acceptLegacy: false` rejects these
132
+ textmode.layer files but still accepts supported textmode.document v2 files.
133
+ The [v2 specification](specification-2.0.md) and schema remain available; their
134
+ wire-format constraints are unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textmode/document",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Portable textmode document format, JSON Schema, types, and dependency-free decoder.",
5
5
  "license": "CC0-1.0",
6
6
  "author": "humanbydefinition <hello@textmode.art> (https://code.textmode.art)",
@@ -32,6 +32,10 @@
32
32
  "./schema/2.0": {
33
33
  "types": "./schemas/2.0/schema.d.ts",
34
34
  "default": "./schemas/2.0/schema.json"
35
+ },
36
+ "./schema/3.0": {
37
+ "types": "./schemas/3.0/schema.d.ts",
38
+ "default": "./schemas/3.0/schema.json"
35
39
  }
36
40
  },
37
41
  "files": [
@@ -39,7 +43,9 @@
39
43
  "dist/**/*.d.ts",
40
44
  "schemas/2.0/schema.json",
41
45
  "schemas/2.0/schema.d.ts",
42
- "docs/*.md"
46
+ "docs/*.md",
47
+ "schemas/3.0/schema.json",
48
+ "schemas/3.0/schema.d.ts"
43
49
  ],
44
50
  "publishConfig": {
45
51
  "access": "public"
@@ -62,10 +68,9 @@
62
68
  "typecheck": "tsc --noEmit -p tsconfig.json",
63
69
  "docs:check": "textmode-docs check",
64
70
  "docs:build": "textmode-docs build",
65
- "test": "vitest run",
71
+ "test": "vitest run --dir tests",
66
72
  "check:package": "node scripts/check-package.mjs",
67
73
  "check": "npm run format:check && npm run lint && npm run lint:markdown && npm run typecheck && npm run docs:check && npm test && npm run build && npm run check:package",
68
- "prepack": "npm run build",
69
74
  "prepare": "npm run build"
70
75
  },
71
76
  "devDependencies": {
@@ -0,0 +1,3 @@
1
+ declare const schema: Readonly<Record<string, unknown>>;
2
+
3
+ export default schema;
@@ -0,0 +1,267 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://textmode.art/schemas/textmode-document-3.0.schema.json",
4
+ "title": "Textmode document 3.0",
5
+ "description": "One or more portable textmode layers in bottom-to-top order. See docs/specification-3.0.md.",
6
+ "$defs": {
7
+ "positiveSafeInteger": {
8
+ "type": "integer",
9
+ "minimum": 1,
10
+ "maximum": 9007199254740991,
11
+ "description": "Positive integer in the interoperable JavaScript safe range."
12
+ },
13
+ "grid": {
14
+ "type": "object",
15
+ "required": ["cols", "rows", "cellWidth", "cellHeight"],
16
+ "properties": {
17
+ "cols": {
18
+ "$ref": "#/$defs/positiveSafeInteger",
19
+ "description": "Positive number of columns."
20
+ },
21
+ "rows": {
22
+ "$ref": "#/$defs/positiveSafeInteger",
23
+ "description": "Positive number of rows."
24
+ },
25
+ "cellWidth": {
26
+ "$ref": "#/$defs/positiveSafeInteger",
27
+ "description": "Positive cell width in pixels."
28
+ },
29
+ "cellHeight": {
30
+ "$ref": "#/$defs/positiveSafeInteger",
31
+ "description": "Positive cell height in pixels."
32
+ }
33
+ },
34
+ "additionalProperties": true,
35
+ "description": "Logical cell count and pixel dimensions of each cell. Products must remain safe integers."
36
+ },
37
+ "rgba": {
38
+ "type": "object",
39
+ "required": ["r", "g", "b", "a"],
40
+ "properties": {
41
+ "r": {
42
+ "type": "integer",
43
+ "minimum": 0,
44
+ "maximum": 255,
45
+ "description": "Red channel."
46
+ },
47
+ "g": {
48
+ "type": "integer",
49
+ "minimum": 0,
50
+ "maximum": 255,
51
+ "description": "Green channel."
52
+ },
53
+ "b": {
54
+ "type": "integer",
55
+ "minimum": 0,
56
+ "maximum": 255,
57
+ "description": "Blue channel."
58
+ },
59
+ "a": {
60
+ "type": "integer",
61
+ "minimum": 0,
62
+ "maximum": 255,
63
+ "description": "Alpha: 0 is transparent and 255 is opaque."
64
+ }
65
+ },
66
+ "additionalProperties": true,
67
+ "description": "Unpremultiplied RGBA integer channels from 0 through 255."
68
+ },
69
+ "color": {
70
+ "oneOf": [
71
+ {
72
+ "type": "string",
73
+ "pattern": "^#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?$"
74
+ },
75
+ {
76
+ "$ref": "#/$defs/rgba"
77
+ }
78
+ ],
79
+ "description": "RGB or RGBA hexadecimal notation, or explicit RGBA channels. Six-digit notation implies alpha 255."
80
+ },
81
+ "transform": {
82
+ "type": "object",
83
+ "required": ["invert", "flipX", "flipY", "rotation"],
84
+ "properties": {
85
+ "invert": {
86
+ "type": "boolean",
87
+ "description": "Glyph inversion; alpha means this cannot generally be replaced by swapping colors."
88
+ },
89
+ "flipX": {
90
+ "type": "boolean",
91
+ "description": "Mirror the glyph horizontally."
92
+ },
93
+ "flipY": {
94
+ "type": "boolean",
95
+ "description": "Mirror the glyph vertically."
96
+ },
97
+ "rotation": {
98
+ "type": "number",
99
+ "description": "Cell rotation in degrees; normalized by the decoder to [0, 360)."
100
+ }
101
+ },
102
+ "additionalProperties": true,
103
+ "description": "Per-cell glyph transforms. Rotation is finite degrees; decoding normalizes it to [0, 360)."
104
+ },
105
+ "cell": {
106
+ "type": "object",
107
+ "required": ["x", "y", "character", "foreground", "background", "transform"],
108
+ "properties": {
109
+ "x": {
110
+ "type": "integer",
111
+ "minimum": 0,
112
+ "maximum": 9007199254740991,
113
+ "description": "Zero-based column index; must equal the position within its row."
114
+ },
115
+ "y": {
116
+ "type": "integer",
117
+ "minimum": 0,
118
+ "maximum": 9007199254740991,
119
+ "description": "Zero-based row index; must equal the row position."
120
+ },
121
+ "character": {
122
+ "type": "string",
123
+ "minLength": 1,
124
+ "maxLength": 64,
125
+ "description": "Nonempty Unicode glyph string, preserved without normalization. The decoder rejects unpaired surrogates and enforces a scalar-count budget."
126
+ },
127
+ "foreground": {
128
+ "$ref": "#/$defs/color",
129
+ "description": "Glyph color with alpha."
130
+ },
131
+ "background": {
132
+ "$ref": "#/$defs/color",
133
+ "description": "Cell background color with alpha. A space with a visible background remains meaningful."
134
+ },
135
+ "transform": {
136
+ "$ref": "#/$defs/transform",
137
+ "description": "Per-cell glyph inversion, flips, and rotation."
138
+ }
139
+ },
140
+ "additionalProperties": true,
141
+ "description": "One cell at its redundant zero-based row and column. Coordinates must match array position; this cross-field rule is checked by the decoder."
142
+ },
143
+ "cells": {
144
+ "type": "object",
145
+ "required": ["encoding", "rows"],
146
+ "properties": {
147
+ "encoding": {
148
+ "const": "object-rows-v1",
149
+ "description": "Tagged cell encoding; v2 supports only object-rows-v1."
150
+ },
151
+ "rows": {
152
+ "type": "array",
153
+ "items": {
154
+ "type": "array",
155
+ "items": {
156
+ "$ref": "#/$defs/cell"
157
+ }
158
+ },
159
+ "description": "Dense row-major arrays; row count and lengths must equal the owning grid. The decoder checks this invariant."
160
+ }
161
+ },
162
+ "additionalProperties": true,
163
+ "description": "Dense row-major cells, including transparent cells. Row count and lengths must equal the grid. See docs/specification-3.0.md#cells-and-coordinates."
164
+ },
165
+ "metadata": {
166
+ "type": "object",
167
+ "required": ["createdAt", "generator"],
168
+ "properties": {
169
+ "createdAt": {
170
+ "type": "string",
171
+ "format": "date-time",
172
+ "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$",
173
+ "description": "Real calendar date and time with uppercase T and timezone, required seconds, optional fractional seconds, and no leap seconds. Enable date-time format assertions in validators."
174
+ },
175
+ "generator": {
176
+ "type": "object",
177
+ "required": ["name", "version"],
178
+ "properties": {
179
+ "name": {
180
+ "type": "string",
181
+ "minLength": 1
182
+ },
183
+ "version": {
184
+ "type": "string",
185
+ "minLength": 1
186
+ }
187
+ },
188
+ "additionalProperties": true,
189
+ "description": "Producer identity; it does not select executable behavior."
190
+ }
191
+ },
192
+ "additionalProperties": true,
193
+ "description": "Optional informational creation and generator metadata. When present, both members are required."
194
+ },
195
+ "layer": {
196
+ "type": "object",
197
+ "required": ["id", "visible", "opacity", "blendMode", "offsetX", "offsetY", "rotationZ", "grid", "cells"],
198
+ "properties": {
199
+ "id": {
200
+ "type": "string",
201
+ "minLength": 1,
202
+ "description": "Nonempty informational source identifier. It is not a persistent cross-document identity; the decoder does not enforce uniqueness."
203
+ },
204
+ "visible": {
205
+ "type": "boolean",
206
+ "description": "Whether the layer participates in presentation. Hidden cells are still stored."
207
+ },
208
+ "opacity": {
209
+ "type": "number",
210
+ "minimum": 0,
211
+ "maximum": 1,
212
+ "description": "Layer opacity from 0 through 1, separate from cell alpha."
213
+ },
214
+ "blendMode": {
215
+ "type": "string",
216
+ "minLength": 1,
217
+ "description": "Nonempty blend mode name. The decoder preserves unknown names; apps decide which they can present."
218
+ },
219
+ "offsetX": { "type": "number", "description": "Horizontal compositing offset in pixels." },
220
+ "offsetY": { "type": "number", "description": "Vertical compositing offset in pixels." },
221
+ "rotationZ": {
222
+ "type": "number",
223
+ "description": "Layer rotation in degrees, clockwise about the layer center in textmode.js; preserved by the decoder."
224
+ },
225
+ "grid": {
226
+ "$ref": "#/$defs/grid",
227
+ "description": "Grid dimensions and cell metrics."
228
+ },
229
+ "cells": {
230
+ "$ref": "#/$defs/cells",
231
+ "description": "Complete dense cell rows."
232
+ }
233
+ },
234
+ "additionalProperties": true,
235
+ "description": "One complete layer, including for documents containing a single layer."
236
+ }
237
+ },
238
+ "type": "object",
239
+ "required": ["format", "formatVersion", "layers"],
240
+ "properties": {
241
+ "format": {
242
+ "const": "textmode.document",
243
+ "description": "Required portable document format discriminator."
244
+ },
245
+ "formatVersion": {
246
+ "const": "3.0.0",
247
+ "description": "On-disk format version, independent of package versions."
248
+ },
249
+ "metadata": {
250
+ "$ref": "#/$defs/metadata",
251
+ "description": "Optional informational timestamp and producer identity."
252
+ },
253
+ "layers": {
254
+ "type": "array",
255
+ "minItems": 1,
256
+ "items": {
257
+ "$ref": "#/$defs/layer"
258
+ },
259
+ "description": "Nonempty ordered layer stack, bottom first."
260
+ },
261
+ "target": false,
262
+ "layer": false,
263
+ "grid": false,
264
+ "canvas": false
265
+ },
266
+ "additionalProperties": true
267
+ }
@@ -1,56 +0,0 @@
1
- # Maintaining the document contract
2
-
3
- ## Ownership
4
-
5
- This repository owns the format specification, versioned JSON Schema, portable
6
- types, decoder, and protocol checks. `textmode.export.js` owns renderer extraction,
7
- selection, generator metadata, serialization options, and downloads. Apps own
8
- placement, history, font selection, and supported-feature decisions.
9
-
10
- The schema describes portable structure and local constraints. The specification
11
- defines meaning and cross-field rules. The decoder implements these rules plus
12
- normalization and caller budgets. Keep all three consistent through contract
13
- tests; do not generate competing schemas or copy protocol logic into callers.
14
-
15
- ## Compatibility
16
-
17
- Package releases and on-disk versions are independent. Preserve the meaning of
18
- `textmode.document@2.0.0`, including its selected/all distinction and legacy
19
- `canvas` field. A new document version needs an explicit reader, specification,
20
- schema, and compatibility tests before it is accepted. Do not infer support for
21
- future `2.x` files. Namespaced or required extensions need a future design.
22
-
23
- Keep published versioned schemas stable. Documentation annotations may improve;
24
- constraint or semantic changes require a compatibility review and explicit
25
- versioning. Changes to normalized results, error codes, paths, migration warnings,
26
- or default limits are also public behavior. Deprecate a JavaScript interface before
27
- removing it from a stable package major.
28
-
29
- ## Tests and documentation
30
-
31
- Fixtures are fixed test inputs. They protect legacy migration and edge cases and
32
- are neither demos nor supported package imports. Keep them in `tests/fixtures`,
33
- out of the package allowlist. Read the fixture README before changing their data.
34
-
35
- Validate every complete `json` block in the consumer guide against the schema
36
- and decoder. Keep syntax fragments in the specification clearly labeled. Run
37
- `npm run check` for formatting, lint, Markdown, types, API docs, tests, build, and
38
- an isolated packed-package check. The docs build disables browser sketch checks
39
- because this package has no browser gallery.
40
-
41
- ## npm releases
42
-
43
- The package is prepared for npm as `@textmode/document`. Its package version is
44
- independent of the on-disk `textmode.document@2.0.0` format version. A package
45
- release does not change the document format.
46
-
47
- Before publishing, run `npm run check` and inspect `npm pack --dry-run`. Confirm
48
- that the archive contains the runtime, declarations, schemas, and consumer docs,
49
- and excludes fixtures and tests. This repository has no automated npm publish
50
- workflow.
51
-
52
- The exporter currently pins this repository at an exact Git commit. When the
53
- registry release is ready for consumer adoption, update the exporter to the exact
54
- published version and regenerate its lockfile with npm. The source repository is
55
- private, so Git-based installs require authenticated read access until consumers
56
- move to the public npm package.