# Skin Contracts (v2) — issue #506 Phase 1

This directory is the authoritative contract surface between **skins** (pure
asset directories) and the **skin-center** (the only loader/renderer, the only
npm package). Skins couple to these contracts only; the skin-center absorbs
every official-DSH coupling behind them.

## Files

| File | What it pins |
| --- | --- |
| `skin-manifest-v2.schema.json` | skin.json v2 structure. Editors/IDEs may point at it; validators must use this local copy and never fetch the `$id` URL. |
| `hooks-api.d.ts` | The `facets.client` escape-hatch API (`x-org.linxin666.skin-center/v1alpha1`): `SkinHooksContext`, `defineSkinHooks()`. |
| `semantic-attrs-v1.md` | The L2 semantic-attribute enumeration (`data-dsh-surface` / `data-dsh-part` / `data-dsh-plugin`) with owner, version and meaning per value. |
| `primary-action-tokens-v1.md` | The filled-primary-button token set (fill / hover / dimmed / foreground), the skin author rules, the loader completion matrix and the warning-only audit. |

The runtime validator (`src/core/manifest-v2/validate.ts`) is the
authoritative fail-closed check; the JSON Schema mirrors it for editors and
external tooling. Keep the two in sync.

## Versioning axes (do not conflate)

- `skinManifestVersion` — **file-structure** version of skin.json. It is
  never a compatibility negotiation axis.
- `facets.client.apiVersion` — the **hooks runtime** contract version,
  negotiated independently. A parseable manifest does not imply an executable
  hooks contract.
- `requires.contracts[]` — declared contract requirements; `optional: true`
  means the skin has a declared degradation path (e.g. runs without hooks).

## Fail-closed with a deprecated allowlist

Unknown fields are hard errors. The v1 fields `package`, `wiring` and
`bodyAttr` are an explicit allowlist: they are ignored with a migration
warning (run the v1→v2 codemod), never an error — otherwise the 11 legacy
manifests would be rejected by their own validator. Optional legal metadata
(`license`, `licenseUrl`, `noticeUrl`, `sourceUrl`, `attribution`) is
first-class in v2.

## Hooks trust model

`hooks.mjs` is trusted executable code that shares this repository's review
and release. It is served verbatim over `GET /skins/<id>/hooks.mjs` only for:

- **built-in skins** — shipped inside the skin-center npm package, same
  review and release by definition;
- **official-market installs** — user-directory skins whose executable identity is byte-verified as reviewed content. Current Workshop installs carry a `dsh-market.provenance.json` whose sha256 pins the on-disk `skin.json` and hooks entry to the bytes the official DSH Market served. Historical installs created before provenance existed may recover only when id, declared entry, complete `skin.json`, and hooks bytes match one identity in generated `src/reviewed-hooks.generated.ts`. The registry is generated from this repository's market skin sources by `scripts/skin-hooks-registry.mjs`; `--check` is part of `skin-center:check`, so any reviewed manifest or hook change must regenerate it. Verification lives in `src/provenance.ts`, is read-only and offline, and fails closed: a foreign source, unknown identity, renamed skin, or an executable-identity mismatch keeps the hooks facet refused with a catalog warning while the declarative parts (skin.css / patches.css / assets) still load (issue #1073).

Locally dropped or third-party skin directories never run hooks unless their executable identity is byte-identical to a reviewed official-market skin. Declarative files outside that identity may be customized without granting additional executable capability.

## Loader-side rules (pinned here, enforced in M2)

- All skin CSS is force-scoped under `html[data-dsh-skin="<id>"]` by the
  loader (lightningcss transform); skins never declare `bodyAttr`.
- CSS whitelist: no `@import`, no remote or protocol-relative URLs, no
  escaping paths, no inline JS; reliance on CSS-Modules hash class names
  (`[class*=...]`) warns. Only relative in-directory assets.
- Every file reference in the manifest is a relative path inside the skin
  directory (no leading slash, no `..`, no protocol URLs).
- The loader re-binds the official shell's `--shiki-background` variable to
  `var(--dsw-alias-markdown-code-block)` on the skin-scoped body, because
  the shell declares it on `:root` where the dark code-block alias (scoped
  to `body[data-ds-dark-theme]`) cannot reach it — without the re-bind,
  Shiki code blocks paint the fixed light background in dark mode (issue
  #826). Skins keep owning the alias values; the loader only restores
  theme-awareness.

## Automatic token fallbacks

The loader ships `official-tokens-v1.json`: a snapshot of every `--dsw-*`
custom property the official shell defines (static palette excluded),
regenerated by `scripts/official-tokens-snapshot.mjs` when the official
frontend moves. For a token a skin does NOT remap, the loader derives a
translucent tint of the skin's own palette via `color-mix()` - the skin's
main color softly blurred over the surface - so new official surfaces keep
the skin's look instead of snapping back to the stock palette. Semantic and
structural groups (buttons, states, masks, shadows, inverted labels, fonts)
are never derived, tokens the skin defines are never overridden, and skins
defining no anchor for a group get no fallback. The derivation is text-only
and resolves through the skin's own remap, so it stays theme-aware (light
and dark) with zero runtime logic.
