# Configuration

Each app repo that consumes a drop has one `node-design-seam.config.json`, normally
at its root. Every command reads it.

```sh
node-design-seam verify                          # ./node-design-seam.config.json
node-design-seam verify --config path/to/it.json # anywhere else
```

**All paths inside the file resolve relative to the config file itself**, not to the
working directory. Running the CLI from a subdirectory does not change what they mean.

## Full example

```jsonc
{
  "dropRoot": "design/wallet-stage-v1",         // the vendored drop folder
  "manifestOut": "ui/src/design/drop-manifest.js",
  "bindingsOut": "ui/src/design/bindings.gen.js",

  "notDropContent": ["AGENTS.md"],              // excluded from hash + scan, by exact name
  "legacyAnchors": [],                          // hand-authored bindings; expected to shrink to zero
  "unwired": []                                 // declared slots knowingly not bound — tracked debt
}
```

## Keys

### `dropRoot`

*string — **required***

The vendored drop folder. Everything under it is walked recursively, hashed, and
scanned; `.DS_Store` and anything named in `notDropContent` are skipped.

`adopt` rewrites this key in place when a new drop is adopted.

### `manifestOut`

*string — **required***

Where `pin` writes the hash manifest. Parent directories are created as needed. The
file is generated JavaScript — commit it, never edit it.

### `bindingsOut`

*string — **required***

Where `bindings` writes the generated binding table. Same rules: generated, committed,
never hand-edited. `verify`'s freshness check re-runs the generator and compares
byte-for-byte.

### `notDropContent`

*string[] — default `["AGENTS.md"]`*

Filenames excluded from both the hash manifest and the slot scan, matched by **exact
name at any depth** — never by pattern or glob.

`AGENTS.md` is the default because it is the drop's authoring contract, carried
forward across drops and read by `verify` for the `contract:` version line. Hashing
it would make carrying it forward an integrity failure.

Setting this key **replaces** the default rather than adding to it. If you add an
entry, include `AGENTS.md` explicitly:

```json
"notDropContent": ["AGENTS.md", "NOTES.md"]
```

### `legacyAnchors`

*object[] — default `[]`*

Hand-authored bindings that survive alongside the generated table, for markup that
predates the scanner. This list is expected to shrink to zero.

```json
"legacyAnchors": [
  {
    "anchor": "[data-bind=\"obs.total\"]",
    "source": "obs.total",
    "as": { "kind": "text" },
    "note": "pre-contract markup; remove when the panel is re-dropped"
  }
]
```

| Field | Type | Notes |
| --- | --- | --- |
| `anchor` | string | **must** match `[name="value"]` |
| `source` | string | dotted path into the snapshot |
| `as` | object | a `BindingWrite` — `{ kind: "text" }`, `{ kind: "attr", name }`, or `{ kind: "presence", hiddenWhenFalsy }` |
| `note` | string, optional | defaults to `"legacy anchor — shrink this list to zero"` |

The `anchor` restriction is validated at load and is not stylistic. Generated anchors
are always a literal attribute-selector token lifted straight off the drop's markup,
and `verify`'s anchor check is a **plain substring test** against the drop's text.
Anything more expressive — a class selector, a descendant combinator, `:hover` —
could never be found there, so it would pass as a binding that resolves to nothing.
Restricting the shape closes the one path that isn't generated.

`loadConfig` throws on a bad entry, naming the offending index:

```
node-design-seam: config legacyAnchors[0].anchor ".kpi span" is not an attribute-selector
(expected the form [name="value"], e.g. [data-bind="obs.total"]) (/abs/path/config.json)
```

### `unwired`

*string[] — default `[]`*

Slot names the drop declares that you have knowingly not bound yet. `verify`'s
contract check treats a listed slot as accounted for instead of failing, and the
coverage line reports the count:

```
verify: contract coverage 5/7 slots bound (2 unwired — tracked debt); views: obs.traces
```

This is a debt register, not a mute button. An unbound, unlisted slot is exactly what
the contract check exists to catch; moving it here should be a decision someone made,
not a way to get to green. In particular, do not park newly-visible slots here after
a package upgrade — see
[Troubleshooting](troubleshooting.md#after-a-package-upgrade).

## Validation errors

`loadConfig` throws before any command runs. All messages are prefixed
`node-design-seam:` and name the absolute config path.

| Message | Cause |
| --- | --- |
| `config is missing required key "dropRoot"` | a required key absent or not a string |
| `config "legacyAnchors" must be an array` | wrong type |
| `config legacyAnchors[i] must be an object` | a string or array entry |
| `config legacyAnchors[i].anchor must be a string` | wrong type |
| `config legacyAnchors[i].anchor "…" is not an attribute-selector` | not `[name="value"]` |
| `config legacyAnchors[i].source must be a string` | wrong type |
| `config legacyAnchors[i].as must be an object` | wrong type |

A malformed JSON file throws from `JSON.parse` with the same `node-design-seam:`
prefix applied by the dispatcher.
