|
| 1 | +# Python Run-time Validator Runner Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Give the Python port a run-time validator runner equal to TypeScript's `runValidators`, and make both runners pass `fixtures/validation-conformance/`. |
| 6 | + |
| 7 | +**Architecture:** One pure function per port — it never raises, it collects every failure as `{field, rule, message, expected?, received?}`. TypeScript's `runValidators` is the contract. The corpus gates boolean verdicts; a new sibling file `runtime-errors.json` pins the exact failure list so the two runners cannot drift in structure or message text. |
| 8 | + |
| 9 | +**Tech Stack:** Python 3 standard library only (`re`), pytest; TypeScript, `bun test`. |
| 10 | + |
| 11 | +**Spec:** no separate spec. The contract is `server/typescript/packages/runtime-ts/src/validator-runner.ts` plus `fixtures/validation-conformance/README.md`. |
| 12 | + |
| 13 | +## Global Constraints |
| 14 | + |
| 15 | +- No new vocabulary, no new validator subtype, no metamodel change. Java is not touched. |
| 16 | +- The Python runner adds no runtime dependency (`PyYAML` stays the only one). |
| 17 | +- ADR-0039: read effective values. Python `attr()` is OWN-only — use `get_meta_attr()` / `children()`. The one own read is `@dbColumnType`. |
| 18 | +- Rules, rule names, field labels and message text are byte-identical in both runners. |
| 19 | +- The behaviour change for TypeScript users goes in `CHANGELOG.md` under `[Unreleased]` (the 1.1 line). |
| 20 | + |
| 21 | +## The rules both runners implement |
| 22 | + |
| 23 | +Per field, in declaration order (effective children): |
| 24 | + |
| 25 | +1. **required** — `mustBePresent = @required | validator.required | assigned primary key with no @default`. An assigned primary key is a field of the primary identity whose `@generation` is neither `increment` nor `uuid`. Absent or null → `{rule: "required", message: "'f' is required"}`. Existing exemptions are unchanged (`partial` and absent; `@default` and absent; `storeFilled` and absent). |
| 26 | +2. Open-bag jsonb string, value-object recursion: unchanged. |
| 27 | +3. **array** (new) — on an array field, `validator.array @min/@max` bounds the element count: |
| 28 | + `{rule: "array", message: "'tags' must have at least 1 items (got 0)", expected: {min: 1}, received: 0}` and `"... at most 3 items (got 4)"` with `expected: {max: 3}`. Applies to scalar arrays and value-object arrays. |
| 29 | +4. **type** — unchanged, plus `field.uri` / `field.inet` must be a string (`expected string`). |
| 30 | +5. **length** — max is strictest-wins: `min(@maxLength, validator.length @max)`. Min is the authored `validator.length @min` when one is authored (`@min: 0` opts out of the floor), else 1 for a declared-required string, else 0. Messages unchanged. Length counts UTF-16 code units in both ports. |
| 31 | +6. **regex** — unchanged (full match). |
| 32 | +7. **numeric** (new) — on `field.int|long|currency|double|float`, `validator.numeric @min/@max`, inclusive: |
| 33 | + `{rule: "numeric", message: "'score' must be at least 0 (got -1)", expected: {min: 0}, received: -1}` and `"... at most 100 (got 101)"` with `expected: {max: 100}`. An int64 passed as a numeric string is compared as an integer and echoed as given. |
| 34 | +8. **format** (new) — unless `@lenient: true`: |
| 35 | + - `field.uri`: strip leading/trailing characters ≤ U+0020; the rest must match `^[A-Za-z][A-Za-z0-9+.-]*:` with a non-empty remainder, and when the remainder starts with `//` the authority (up to the next `/`, `?` or `#`) must be non-empty. Failure: `{rule: "format", message: "'website' must be an absolute URI", expected: "uri", received: value}`. |
| 36 | + - `field.inet`: must match the IPv4 or IPv6 literal patterns from `codegen-ts/src/templates/net-regex.ts`. Failure: `{rule: "format", message: "'sourceIp' must be an IPv4 or IPv6 address", expected: "inet", received: value}`. |
| 37 | + |
| 38 | +A type failure stops further checks on that value. Order of failures within one value: length max, length min, regex, numeric min, numeric max, format. |
| 39 | + |
| 40 | +## Review Focus |
| 41 | + |
| 42 | +- Python `bool` is an `int`: `True` on a numeric field must be a type failure, as in TypeScript. |
| 43 | +- A `1.0` bound must print as `1`, as JavaScript prints it. |
| 44 | +- A non-BMP character counts as 2 toward length in both ports. |
| 45 | +- An invalid `@pattern` yields a `regex` failure, never an exception. |
| 46 | +- `partial=True` with an absent assigned primary key is not a failure; a present `None` is. |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +### Task 1: Pin the failure list — `fixtures/validation-conformance/runtime-errors.json` |
| 51 | + |
| 52 | +- [ ] Add `runtime-errors.json`: `{ "errors": { "<case name>": [ {field, rule, message, expected?, received?} ] } }`, one entry per `expectValid: false` case in `cases.json`. |
| 53 | +- [ ] Document the file and the two run-time runners in the corpus `README.md`. |
| 54 | + |
| 55 | +### Task 2: TypeScript `runValidators` — corpus + new rules |
| 56 | + |
| 57 | +**Files:** modify `server/typescript/packages/runtime-ts/src/validator-runner.ts`; tests in `server/typescript/packages/runtime-ts/test/validator-runner.test.ts`; create `server/typescript/packages/integration-tests/test/validation-conformance-runtime.test.ts`; modify `scripts/ci-local.sh` (`gate_conf_ts`). |
| 58 | + |
| 59 | +- [ ] Write the corpus runner test: load `meta.json`, run each case through `runValidators`, assert `result.ok === expectValid`, and for a failing case assert `result.errors` deep-equals `runtime-errors.json`. Run it; expect the uri/inet/numeric/array/length/assigned-PK cases to fail. |
| 60 | +- [ ] Add unit tests for rules 1, 3, 5, 7, 8 and the Review Focus lines that apply to TypeScript. Run; expect failures. |
| 61 | +- [ ] Implement the rules. Run both test files; expect green. Run `bun test` in `runtime-ts` and `bun run --filter '*' typecheck`. |
| 62 | +- [ ] Commit. |
| 63 | + |
| 64 | +### Task 3: Python `run_validators` |
| 65 | + |
| 66 | +**Files:** create `server/python/src/metaobjects/runtime/validator_runner.py`; modify `server/python/src/metaobjects/runtime/__init__.py` and `object_manager.py` (`ObjectManager.validate`); tests `server/python/tests/runtime/test_validator_runner.py` and `server/python/tests/runtime/test_validation_conformance_runtime.py`. |
| 67 | + |
| 68 | +**Interfaces:** |
| 69 | + |
| 70 | +```python |
| 71 | +@dataclass(frozen=True) |
| 72 | +class ValidationFailure: |
| 73 | + field: str |
| 74 | + rule: str |
| 75 | + message: str |
| 76 | + expected: object = None |
| 77 | + received: object = None |
| 78 | + def to_dict(self) -> dict[str, object]: ... # omits expected/received when None |
| 79 | + |
| 80 | +@dataclass(frozen=True) |
| 81 | +class ValidationResult: |
| 82 | + ok: bool |
| 83 | + errors: tuple[ValidationFailure, ...] = () |
| 84 | + |
| 85 | +def run_validators(entity: MetaData, data: Mapping[str, object], *, |
| 86 | + partial: bool = False, store_filled: Sequence[str] = ()) -> ValidationResult: ... |
| 87 | + |
| 88 | +class ObjectManager: |
| 89 | + def validate(self, entity_name: str, data: Mapping[str, object]) -> ValidationResult: ... |
| 90 | +``` |
| 91 | + |
| 92 | +- [ ] Write the corpus runner test (same assertions as Task 2, `to_dict()` compared with `runtime-errors.json`) and the unit tests, porting `validator-runner.test.ts` case for case plus the Review Focus lines. Run; expect import failure. |
| 93 | +- [ ] Implement. Run `uv run pytest tests/runtime -q`, `uv run mypy`, `uv run ruff check`; expect green. |
| 94 | +- [ ] Commit. |
| 95 | + |
| 96 | +### Task 4: Docs and changelog |
| 97 | + |
| 98 | +- [ ] `docs/ports/python.md`: document `run_validators` and `ObjectManager.validate`. |
| 99 | +- [ ] `docs/CONFORMANCE.md`: correct the corpus case count and note the two run-time runners. |
| 100 | +- [ ] `CHANGELOG.md` `[Unreleased]`: Added (Python runner), Changed (TypeScript `runValidators` now enforces numeric, array, uri/inet format, assigned-PK presence, strictest-wins max length, authored `@min` over the floor — a behaviour change). |
| 101 | +- [ ] Commit. |
| 102 | + |
| 103 | +### Task 5: Verify |
| 104 | + |
| 105 | +- [ ] `scripts/ci-local.sh` (full) green; one independent review of the branch; fix findings. |
0 commit comments