# Item `validator` result

## Files written
- `src/validate.ts` (new)
- `test/validation.test.ts` (new)

No other file was created or changed. `docs/architecture.md`, `src/types.ts` and `package.json` were not touched.

## Exports
`src/validate.ts` exports one function: `validateRollups(input: unknown): ValidationResult`. Its only import is `import type` from `./types.ts`.

- If the input is not an array, it returns a single `invalid_type` issue at `$`. An empty array is valid.
- A row must be a plain object: one whose prototype is `Object.prototype` or `null`. That covers literals, `JSON.parse` output and `Object.create(null)`. Anything else (null, arrays, primitives, `Date`, `Map`, class instances, objects with a custom prototype) gets `invalid_type` at `$[i]`, with no errors invented for its fields.
- Fields are checked in the schema order of `src/types.ts`. A required field must be an own property (`Object.hasOwn`), otherwise the code is `required`. Fields inherited from `Object.prototype` do not count. A field that is present but `undefined` gets `invalid_type`.
- Unknown own enumerable keys get `unknown_field`. They are reported after the schema fields, in the object's own key order. Keys that look like identifiers (`/^[A-Za-z_$][A-Za-z0-9_$]*$/`) use dot paths; all other keys use JSON-quoted brackets, e.g. `$[0]["odd.key"]`.
- `name`: a non-string gets `invalid_type`. An empty string or one with surrounding whitespace (`trim() !== value`) gets `invalid_value`. A repeat of an earlier valid name gets `duplicate_name` on the later row. The comparison is case-sensitive, and a name counts even if its row has other errors.
- `stage`: not a number → `invalid_type`; a number other than 0, 1 or 2 → `invalid_value`.
- `proofSystem` and `dataAvailability`: not a string → `invalid_type`; any other string → `invalid_value`.
- `permissionlessProving` and `forceInclusion`: anything other than a boolean is `invalid_type`. No coercion.
- `exitWindowDays`: not a number → `invalid_type`; NaN, ±Infinity or a negative number → `invalid_value`. Fractions are allowed.
- `upgrades`:
  - If it is not a plain object, the result is `invalid_type` and the validator does not look inside it.
  - `.type` is checked first: missing → `required`, not a string → `invalid_type`, an unknown value → `invalid_value`. If `.type` is invalid, no other issues are reported for this object.
  - For `securityCouncil`, `threshold` and `members` must be present (`required`), numbers (`invalid_type`) and positive safe integers (`invalid_value`). If both pass, `threshold > members` gives `invalid_value` at `.threshold`.
  - Extra keys get `unknown_field`. That includes `threshold` or `members` on a `multisig` or `singleKey`.
- There are no cross-factor rules, and the input is never mutated or coerced.
- Result: `{ valid: true, rollups: input.slice() }` (a new array holding the same objects), or `{ valid: false, errors }` with every issue in traversal order. Each issue is `{ path, code, message }` with a clear message.

## Commands run
Node on this machine is v22.23.2, which strips types by default. Node 24 is not installed.

| Command | Result |
|---|---|
| `node --test test/validation.test.ts` | 24 tests: 24 pass, 0 fail |
| `node src/validate.ts` | exit 0, nothing printed (loads cleanly) |

The first run had 2 failures, both mistakes in the tests rather than the validator. The tests had assumed `Object.create(proto)` counts as a plain object. They now expect `invalid_type`, and a separate test temporarily pollutes `Object.prototype` (restored in a `finally` block) to show inherited fields are still reported as `required`.

The tests cover:
- the valid empty array, null and other non-array input, non-object rows
- prototype edge cases: `__proto__` and `constructor` keys from JSON, null prototypes, class instances
- every field rule and every enum value, including both sides of each boundary
- the upgrade discriminator, extra keys on each variant, council numbers and the threshold/members relationship
- path quoting, unknown-key order, and duplicate names
- several errors in one row and errors across rows
- no mutation of the input

The tests create no temp files and use no network.

## What could not run
- `npm test` (all suites) was not run: the sibling modules (`score.ts`, `rank.ts`, `cli.ts`) belong to other items in this wave and are not in this checkout. This item does not depend on them.
- Nothing was tested on Node 24, because it is not installed. The code uses only erasable syntax and `node:` built-ins.

## Deviations from docs/architecture.md
None. Two choices the architecture leaves open:
1. "Object" is read as a plain object. Objects with a non-`Object.prototype`, non-null prototype are rejected as `invalid_type`, because the brief asks for plain objects.
2. A valid name on a row that has other errors still counts toward duplicate detection.
