Metadata-Version: 2.5
Name: timeskeleton
Version: 0.6.0
Summary: The TimeSkeleton manifest schema, its fixtures and a validator
Project-URL: Repository, https://github.com/TimeToAlign/timeskeleton
Project-URL: Issues, https://github.com/TimeToAlign/timeskeleton/issues
License-Expression: MIT
Keywords: json-schema,music,timeline,timetoalign
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: File Formats :: JSON :: JSON Schema
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: jsonschema<5,>=4.26.0
Description-Content-Type: text/markdown

# timeskeleton

TimeSkeleton is the JSON Schema for the structural serialization of musical
timelines, with manifest fixtures emitted by
[timetoalign](https://github.com/timetoalign/timetoalign) and validation
packages for Python and TypeScript. It carries timeline structure, not event
data.

**Status: alpha.** The repository contains schema versions 0.5.0 and 0.6.0;
0.6.0 is the current version. It provides validators that locate a violation under one shared path
rule, a canonical manifest order, standard writers that produce the same bytes
in Python and TypeScript, a lossless JSON reader for TypeScript, generated
TypeScript types, a migration from 0.5.0 to 0.6.0, and shared fixtures. It
also specifies how a manifest is drawn, and both packages ship that
specification.

## The manifest

A TimeSkeleton file is one manifest: a JSON object with `meta`, `timeline`,
and an optional `graphical` block. `meta.schema_version` is `"0.6.0"`, and
`meta.generator` is optional. `timeline` is one timeline manifest matching
timetoalign's `Timeline.to_dict(events=False)` output: its axis (`class`,
`unit`, `number_type`, and `length`), child timelines at exact offsets,
conversion maps, regions, flow control, measure map, and metric hierarchy.
The schema identifier is
[`https://timetoalign.github.io/timeskeleton/0.6.0/timeskeleton.schema.json`](https://timetoalign.github.io/timeskeleton/0.6.0/timeskeleton.schema.json).

A manifest describes exactly one top-level timeline and all its descendants.
It says nothing about where or how often an application places that timeline.
Event tables travel separately, typically as Arrow or Parquet data, and are
referenced rather than embedded.

Every coordinate-valued member is an object with `unit`, `number_type`, and
`value`. Its number type selects the representation: an integer is a JSON
integer, a float is a JSON number, and a fraction is a `[numerator,
denominator]` pair of JSON integers with a strictly positive denominator,
mirroring BOPP.

Every timeline, conversion map, region, flow-control break, jump and marker
carries an `id`, which identifies it; a `name`, where an element has one, is
what is displayed, and it may repeat. An id is unique within the manifest across all of
these kinds; it is a non-empty string without `/` and without control
characters, and it may contain spaces. Flow-control `markers` is a
list of `{id, name, coordinate}` objects, and a jump's `target` is the id of a
marker of the same timeline, or `null`. The JSON Schema cannot express either
rule, so both validators check them after the schema: one problem per
duplicate id, located at its second occurrence, then one per jump whose
`target` names no marker of its timeline, located at that `target`.

A measure map is the
[Measure Map standard](https://github.com/measure-map/specification)'s
compressed row array: an entry equal to the preceding entry's default
successor is omitted. A row is the standard's Measure plus two extensions:
the integer `volta`, and `offset_within_measure`, where the row begins inside
its nominal measure in quarter notes. Lengths are strictly positive and
`qstamp` is non-negative; exact Fraction pairs are permitted wherever the
standard uses JSON numbers. Only a timeline whose unit is `quarters` may
carry a measure map.

The schema is the source of truth for timetoalign and TimeLineEditor.
timetoalign's hand-written serialization is tested for schema conformance and
fixture round trips. TypeScript types are generated in this repository.

## Drawing a manifest

The optional `graphical` block contains named declarations in `styles` and an
ordered list of `statements`:

```json
"graphical": {
  "styles": {
    "highlight": {"stroke": "#7F00FF", "fill": "#7F00FF"}
  },
  "statements": [
    {"select": {"element": "label"}, "set": {"fill": "#404040"}},
    {"select": {"element": "wedge", "within": "performance"}, "set": "highlight"}
  ]
}
```

Each statement has a selector in `select` and either a declaration or a style
name in `set`. Statement order matters. The complete selectors, presentation
properties, cascade, defaults, element addresses, layout, and writer profiles
are defined by the
[drawing specification](https://github.com/TimeToAlign/timeskeleton/blob/main/docs/drawing.md).

A statement may name a style missing from its block's `styles`. Python's
`complete_styles(graphical)` and npm's `completeStyles(graphical)` return a
copied block with every such name added as an empty declaration, together with
the sorted missing names. Python issues one `MissingStyleWarning` and npm one
`console.warn` call, each naming all missing styles. The completed block is
valid, and a statement that names one of those empty declarations sets nothing.

A drawing is derived and is not exchanged as JSON. The drawing specification
pins the drawing, and both packages contain it: the wheel as
`timeskeleton/docs/drawing.md`, the npm package as
`@timetoalign/timeskeleton/docs/drawing.md`.

Every text is measured and drawn with the bundled DejaVu Sans 2.37 release
file, `fonts/DejaVuSans.ttf`. `fonts/LICENSE` contains its licence, and
`fonts/dejavu-sans-2.37.metrics.json` contains its advance-width metrics.
Python exposes the files through `font_path()` and `font_metrics()`. The npm
package exports `fontMetrics` and exposes the files through the
`@timetoalign/timeskeleton/fonts/*` subpath. Run
`scripts/extract_font_metrics.py` to regenerate the metrics from the font.

## Canonical order

Readers accept a manifest's collections in any order. Writers emit one
canonical order, so equal manifests have equal text. The keys of every object,
at every depth, are sorted by Unicode code point. The arrays whose order keys
cannot express are put in this order:

- `conversion_maps` by id;
- `regions` by start, then end, then id;
- flow-control `breaks` and `markers` by coordinate, then id;
- flow-control `jumps` by from coordinate, then to coordinate, then id.

Ids are unique within a manifest, so no two entries tie.

`children` is an object keyed by id, so its text is sorted by id; its
canonical order, the one a reader that traverses the tree follows, is by
offset, then id. Coordinates compare by exact numeric value, never by JSON
spelling. Strings compare by Unicode code point. No other
array is reordered: `graphical` statements, conversion-map payloads, and
measure-map rows keep their order.

## The standard writer

Each package has one writer, `dumps`. It puts a manifest in canonical order,
sorts the keys of every object by code point, and writes UTF-8 text with
two-space indentation and a final newline. It writes any other JSON value, such
as an application's file embedding manifests, under the same rules. Every
character is unescaped except `"`, `\`, and control characters U+0000 to
U+001F. A float remains a float and is spelled as Python's `repr` spells it
(`2.0`, `0.0001`, `1e-05`, `1e+16`, `-0.0`); an integer never has a decimal
point or exponent. A NaN, an infinity, or a string containing a lone surrogate
is refused. The fixtures use this writer.

## Install

```bash
pip install timeskeleton
```

```bash
pnpm add @timetoalign/timeskeleton
# or
npm install @timetoalign/timeskeleton
```

## Usage

### Python

Python validation raises `SchemaValidationError` with a tuple path to the
offending value. Read a manifest with `json.loads`, which keeps integers and
floats distinct and every integer exact, and write it with `dumps`:

```python
import json

from timeskeleton import SchemaValidationError, dumps, fixture_paths, validate

value = json.loads(fixture_paths()[0].read_text(encoding="utf-8"))
try:
    validate(value)
except SchemaValidationError as error:
    print(error.path)
else:
    text = dumps(value)
```

- `validate(value)` checks a manifest against the schema of the version it
  declares, 0.5.0 or 0.6.0. A value that declares neither is checked against
  0.6.0 and is rejected at `meta.schema_version`.
- `migrate(value)` returns a copy of a manifest of any supported version
  rewritten to 0.6.0, through one step per later version; the input is not
  changed and the result is not validated. From 0.5.0 only
  `meta.schema_version` changes, since the two versions share one manifest
  format. A value that declares no supported version raises
  `UnsupportedVersionError`, as do the version-taking accessors below.
- `dumps(value)` returns the standard text of a manifest or any JSON value.
- `canonical_order(value)` returns a deep copy with the manifest's arrays in
  canonical order. `canonical_json(value)` returns compact JSON with object
  keys sorted by code point.
- `identity_problems(value)` returns every violation of the identity rules in
  a schema-valid manifest, in the order described above; `validate` raises the
  first.
- `complete_styles(graphical)` returns `(completed, missing)` and warns with
  `MissingStyleWarning` when `missing` is non-empty.
- `SCHEMA_VERSION` is `"0.6.0"`, and `SUPPORTED_VERSIONS` is
  `("0.5.0", "0.6.0")`.
  `declared_version(value)` returns `meta.schema_version` when it is a string.
- `schema_path()` and `load_schema()` return the bundled schema;
  `fixture_paths()` and `rejection_paths()` return the bundled manifest and
  rejection fixtures. Each takes a version, 0.6.0 by default.
- `font_path()` returns the bundled TrueType file, and `font_metrics()` returns
  its metrics.

### TypeScript

`JSON.parse` cannot preserve every distinction in the format: it reads `2.0`
as the integer `2`, rounds integers beyond 2^53, and changes the enumeration
order of array-index keys. Use the package's lossless reader:

```ts
import {
  dumps,
  parse,
  SchemaValidationError,
} from "@timetoalign/timeskeleton";

function load(text: string): string {
  try {
    return dumps(parse(text));
  } catch (error) {
    if (error instanceof SchemaValidationError) {
      console.error(error.path);
    }
    throw error;
  }
}
```

- `parse(text)` reads and validates a manifest without losing a number or key
  order. `parseJson(text)` reads any JSON text without validating it.
- `validate(value)` narrows an unknown value to `TimeSkeletonManifest`,
  checking it against the schema of the version it declares.
- `migrate(value)` returns a copy rewritten to 0.6.0, as Python's `migrate`
  does, keeping every number and key order `parseJson` read.
- `dumps(value)` is the standard writer. `compactJson(value)` writes without
  whitespace, and `canonicalJson(value)` also sorts object keys by code point.
- `canonicalOrder(value)` returns a deep copy with the manifest's arrays in
  canonical order.
- `identityProblems(value)` returns the same identity problems as Python's
  `identity_problems`.
- `completeStyles(graphical)` returns `{graphical: completed, missing}`.
- `fontMetrics` contains the bundled font's advance-width metrics.
- `schema`, `schemas`, `SCHEMA_VERSION`, and `SUPPORTED_VERSIONS` expose the
  bundled 0.5.0 and 0.6.0 contracts. The generated types include
  `TimeSkeletonManifest`, `TimelineManifest`, `Graphical`, `Statement`,
  `Selector`, and `Declaration`.

A number read by `parseJson` is a `JsonNumber`:

| JSON text | `JsonNumber` |
| --- | --- |
| an integer within ±(2^53 − 1), such as `480` | a `number` |
| an integer beyond that range, such as `9007199254740993` | a `bigint` |
| a float with a fractional part, such as `0.5` | a `number` |
| an integral float, such as `2.0`, `1e+16`, or `-0.0` | a `JsonFloat` |

`Lossless<T>` replaces every `number` in `T` with `JsonNumber`. A float whose
value is an integer must be passed as `new JsonFloat(2)`, not as `2`, if it is
to be written as a float. JavaScript code that writes manifests must use
`dumps` or `compactJson`, not `JSON.stringify`.

## Versioning

Schema 0.5.0 is the earliest version this repository validates, and every
schema version from 0.5.0 onward remains validatable. Each later version has
a pure, ordered migration from the version before it in both packages,
pinned by a before-and-after fixture pair under `fixtures/migrations/`.
0.6.0 keeps the manifest format of 0.5.0 and revises the drawing
specification; its migration rewrites `meta.schema_version` alone.

A timeskeleton release is a schema-version event: one `vX.Y.Z` tag publishes
both packages through trusted publishing. `scripts/check_versions.py` checks
that all version declarations agree.

## Fixtures

The [fixture guide](fixtures/README.md) describes the generated manifests and
drawings of each version, the hand-authored rejection cases, and the migration
pairs. Manifest fixtures are emitted
only by `scripts/emit_fixtures.py` using timetoalign and record its version and
commit in `meta.generator`. Do not edit them by hand.

## Development

Clone the repository, then run the package commands in their directories:

```bash
cd python
uv sync
uv run ruff check .
uv run ruff format --check .
uv run pytest -q
uv build
```

```bash
cd ../js
pnpm install --frozen-lockfile
pnpm lint
pnpm typecheck
pnpm test
pnpm generate
pnpm build
```

To regenerate the font metrics, run this from `python/`:

```bash
uv run python ../scripts/extract_font_metrics.py
```

To re-emit manifests, use an interpreter containing the timetoalign revision
being represented:

```bash
python scripts/emit_fixtures.py --timetoalign-commit "$(git -C /path/to/timetoalign rev-parse --short HEAD)"
```

From the repository root, check version declarations with:

```bash
python3 scripts/check_versions.py
```

## Releasing

See the
[contribution guide](https://github.com/TimeToAlign/timeskeleton/blob/main/CONTRIBUTING.md)
for the required version updates, checks, tag, and trusted-publishing
configuration.

## License

[MIT](https://github.com/TimeToAlign/timeskeleton/blob/main/LICENSE).
