# 1.8.0 — `validate`: a read-only draft linter

Minor release. One new command, zero changes to existing behavior. `validate`
catches a corrupt or invariant-broken CapCut draft **before** you open CapCut —
turning "why is my draft blank / why won't it open / why did the captions
vanish" into a one-line preflight.

## Highlights

- 🔍 **`capcut-david validate <project>`** — lints `draft_content.json` for
  dangling references, orphans, duplicate ids, zero/under/overrun durations,
  overlapping clips, missing companions, canvas sanity, and (for a directory)
  CapCut registration / sidecar health. Prints a versioned JSON report or, with
  `-H`, a human table.
- 🛡️ **Strictly read-only.** No `saveDraft`, no `.bak`, no `mkdir` — locked by a
  byte-identity test (bytes + mtime unchanged after a run). It is **not** a write
  command, so it runs fine while CapCut is open.
- 🚦 **CI-friendly exit codes.** `0` clean · `2` problems found · `1` tool
  failure. `-q` prints nothing and gives you the exit code only — drop
  `capcut-david validate <draft> -q && …` in front of the CapCut-open step of any
  pipeline.
- 🧱 **Diagnostic backbone.** Every detector is a pure, exported `CHECKS`
  function emitting a stable finding schema (`id` / `severity` / `fixable` /
  `fix_hint`). The future `--fix`, `gc`, `sync-timelines`, and `relink` verbs
  reuse these same ids.
- ✅ **331 tracked tests** (+52). `validate.js` coverage 97.6% lines / 100%
  functions. Typecheck + lint clean.

## Usage

```
$ capcut-david validate my-draft            # JSON report, exit 0/2
$ capcut-david validate my-draft -H         # human table grouped by severity
$ capcut-david validate my-draft -q         # exit code only (CI gate)
$ capcut-david validate my-draft --strict   # warnings also fail (exit 2)
$ capcut-david validate my-draft --check-assets --check-timelines
$ capcut-david validate my-draft --id materials.dangling_ref   # one check
```

JSON envelope:

```json
{
  "schema": "capcut-david/validate@1",
  "ok": false,
  "project": "C:\\…\\My Draft",
  "draft_file": "C:\\…\\My Draft\\draft_content.json",
  "summary": { "errors": 1, "warnings": 0, "info": 2, "checks_run": 13, "checks_skipped": 2 },
  "findings": [
    { "id": "materials.dangling_ref", "severity": "error",
      "message": "segment 8af2… references missing material 9c01…",
      "location": { "kind": "segment", "ref": "8af2…" },
      "fixable": false, "fix_hint": null }
  ]
}
```

## The checks

**Always on (pure graph — CI-safe):**

| id | severity | catches |
|---|---|---|
| `materials.dangling_ref` | error | a `segment.material_id` resolving to no material (exact-id match — never a prefix collision) |
| `materials.duplicate_id` | error | the same material id in two slots (a globally-unique GUID appearing twice = corruption) |
| `companions.missing` | warning | a present `extra_material_refs` entry that resolves to nothing |
| `segments.zero_duration` | error / warning | `duration ≤ 0` (error on video/audio/text, warning on aux tracks) |
| `duration.underrun` | warning | declared `duration` shorter than the last segment end |
| `duration.overrun` | info | declared `duration` longer than content (normal after a trim) |
| `segments.overlap` | warning | two clips overlapping on one video/audio track (text/overlay stack legitimately) |
| `materials.orphan_text` | info | a text material no segment references (e.g. an `import-captions` leftover) |
| `materials.orphan_media` | info | a video/audio material no segment references |
| `canvas.config_sanity` | warning | non-positive `fps` (read at the draft **root**) or canvas width/height |

**Filesystem — automatic when a directory is passed** (a bare `.json` file → reported *skipped*, never error):

| id | severity | catches |
|---|---|---|
| `meta.missing` | error | no `draft_meta_info.json` → invisible to CapCut, `register` fails |
| `meta.unregistered` | warning | draft dir absent from `root_meta_info.json` |
| `meta.duplicate_draft_id` | warning | one `draft_id` shared by two folders (a `cp -r` collision) |

**Filesystem — opt-in:**

| id | flag | catches |
|---|---|---|
| `assets.missing_file` | `--check-assets` | an absolute media `path` pointing at a missing file (skips `##…##` placeholder tokens) |
| `timelines.divergence` | `--check-timelines` | a `Timelines/<guid>/draft_content.json` mirror that diverges from root on a cheap duration/segment-count signal |

`--projects-root <dir>` points the `meta.*` checks at a non-default CapCut install.

## Not in this release

`effect.bind_dangling` is intentionally deferred: video FX bind via `material_id`
on an `effect` track, while a filter material's `bind_segment_id` is legitimately
empty (`""` = track-wide). Shipping it without handling both routing modes would
produce false positives — it returns once a fix verb needs it.

## Migration

**None.** `validate` is additive and read-only. No existing command changes
output. Pipelines can adopt it incrementally as a preflight gate.

## Compatibility

- CapCut ≥ 5.x desktop (Windows + macOS), JianYing 6+ unsupported — unchanged.
- Node `>= 18` — unchanged.
- Runtime dependencies: zero — unchanged.

## Roadmap (1.x — non-binding)

- `1.x.0` — `validate --fix` / `gc` / `sync-timelines` / `relink` (built on these check ids)
- `1.x.0` — `capcut-david query` (animation / sticker / effect / filter catalogue lookup)
- `1.x.0` — JianYing 6+ research; `psycho-build` dynamic audio ducking
