# @akari-video/edit-lint

`edit-lint` validates an AKARI Video `edit.json` project and writes the canonical result to
`.akari/lint.json` plus a human-readable report under `.akari/reports/`.
The nearest ancestor with a project `.akari/` is used; the application-wide `AKARI_HOME` is excluded.

```sh
node packages/edit-lint/bin/edit-lint.mjs <project-root|edit.json> [--json]
```

Exit code `0` means PASS, `1` means lint findings include an error, and `2` means the lint command
could not run.

## Overlay fragment checks

- `overlays.html-root`: errors unless a referenced overlay HTML fragment has exactly one balanced
  root element.
- `overlays.data-attributes`: errors when a fragment root's `data-start` or `data-duration` does not
  match `edit.json`.
- `overlays.root-data-attributes`: warns whenever a fragment root declares `data-start` or
  `data-duration`; `edit.json` is the source of truth, and animation delays inside a fragment use
  local seconds starting at clip time 0.
- `overlays.keyframes-sparse`: warns when a multi-step keyframes rule omits an animated property
  from its 0% or 100% endpoint.
- `overlays.base-hidden-state`: warns when a selector has a hidden base state but its animation
  ends visible; keep the final resting state in the base rule and put the hidden state at 0%.
- `overlays.preserve-3d-opacity-animation`: warns when one selector combines `preserve-3d` with an
  opacity animation; move opacity to a parent so Blink does not flatten the 3D element.

## Media checks (`--media`)

Media decoding is opt-in. Without `--media`, edit-lint does not probe or decode media for these
checks and records `media checks require --media` in `skipped[]`.

```sh
node packages/edit-lint/bin/edit-lint.mjs <project-root> --media --json
```

With `--media`, edit-lint inspects every `sources[]` entry referenced by a visual or audio media
item. Each source is probed once, and media finding paths include the source ID.

- `media.silence`: detects audio intervals at or below -50 dB for at least 0.5 seconds. Findings are
  warnings unless `--silence-error-seconds` promotes long intervals to errors.
- `media.volume`: reports mean and maximum volume. Findings are warnings unless
  `--max-volume-error-db` promotes an over-limit maximum to an error.
- `media.source-range`: errors when a media item asks for a span the source does not have. Every
  lane is covered (visual cuts, `bgm` / `narration` / `sfx` items, nested bag items) plus the v1
  `audio.*` declarations, and the container duration is used so sources without an audio stream are
  checked too. `source.in` at or beyond the duration and an effective `source.out` past the duration
  are both errors; sources whose container duration is unavailable are skipped with a reason.
- `media.audio-shorter-than-out`: warns for each visual media item whose effective source `out`
  exceeds that source's audio-stream duration while staying inside the container duration (the
  source plays but its audio ends early). The message includes the missing duration. Items that run
  past the container duration are reported by `media.source-range` instead.
- `media.caption-silence-coverage`: warns when more than 30% of caption display time overlaps a
  silence interval of at least one second. `--caption-silence-warn-percent` changes the threshold.
- `media.crop-scale-proxy-ratio`: warns when a cropped media item's `transform.scale` equals its
  source's original-to-proxy dimension ratio. That value was the workaround while preview decoded
  the proxy and export preferred it; now that both decode the original it enlarges the framing by
  the ratio. Needs the dimensions of both files, so it only runs under `--media`; sources without a
  `proxy`, items without a `crop` or a declared `scale`, equal-size proxies (a ratio of 1 cannot be
  told apart from the default `scale`), and files whose dimensions cannot be read are all skipped.
- `audio.narration.trim`: warns when a narration media item's `in` is at or beyond its audio-stream
  duration but still inside the container duration. Invalid or reversed narration `in`/`out` values
  are errors even without `--media`; an `in` past the container duration is a `media.source-range`
  error.

Silence and volume checks are skipped for a source without an audio stream or when ffprobe cannot
inspect it. Duration-dependent checks are skipped when the audio-stream duration is unavailable.
One ffprobe invocation per source path reads both the container duration and the first audio
stream's duration.
Caption/silence coverage runs only when the captions can be associated with exactly one referenced
visual source: either every caption declares the same `src`, or the timeline references a single
visual source and captions do not contradict it. Missing, empty, or multi-source/ambiguous captions
are skipped with a reason.

`timeline.items.order` is a default-path warning when a track's `items[]` are not stored in
non-decreasing `at` order. It does not change the render meaning or fail lint.

`timeline.duration-derived` is an info finding when a project without visual media derives its duration from overlays, captions, narration, or SFX; BGM never extends that fallback duration.

For v2, `at` and `duration` are frames; `source.in`, `source.out`, and fade values are seconds.
`v2.item-duration-short` warns when a non-audio item is shorter than half a second (excluding
caption items). `v2.item-duration-source-mismatch` warns when a media item's source interval,
after speed adjustment, differs from its timeline duration by at least fivefold. The latter
also applies to audio items but excludes still images and frozen sources. Both are warnings
and leave the pass/fail verdict unchanged.

## Engine compatibility (`--engine`)

Before export, check v2 projects against the selected renderer. Use `auto` when engine selection is
also automatic.

```sh
node packages/edit-lint/bin/edit-lint.mjs <project-root> --engine gpu
node packages/edit-lint/bin/edit-lint.mjs <project-root> --engine auto
```

The field-level source of truth is
[`packages/schemas/engine-capabilities.json`](../schemas/engine-capabilities.json). Its statuses mean:

- `consumed`: the selected engine consumes the field.
- `partial`: support is approximate or limited; `engine.partial-field` emits a warning.
- `ignored`: the field is not rendered; `engine.unsupported-field` emits an error.
- `other-subsystem`: a subsystem outside the visual engine consumes the field.

If a canonical field has no matching row for an item's projection, `engine.capability-unknown` emits
a warning so table drift is visible. With `--engine auto`, identical GPU and OSR conclusions are
collapsed under a `gpu/osr:` prefix; different conclusions are reported separately.

```text
engine.unsupported-field [error] gpu/osr: tracks[].items[].perspective is not consumed
  (tracks[0].items[1]; it will not affect rendering). hint: move it to the layers path or remove it
```

Without `--engine`, the capability table is not read and the default findings, skipped checks, and
`.akari/lint.json` remain on the existing compatibility path.
