Files
projax/internal
mAi df65e4b586 feat(itemwrite): introduce internal/itemwrite/ validator
Phase 5c slice A. Pulls the structural rules out of the Postgres
triggers into a Go-side validator. The trigger stays as defence in
depth; the validator is the human-facing error path.

- docs/plans/itemwrite-validation.md enumerates every rule the
  triggers in 0001 + 0010 enforce, with the ValidationError.Kind
  callers will see for each. Eleven rules total (two SQL-only safety
  rails kept untranslated).
- internal/itemwrite/itemwrite.go: ValidationError + Input + Reader
  interface + ValidateFormat (pure: missing fields, slug format,
  status whitelist, self-parent) + ValidateAgainstStore (DB-aware:
  unknown-parent, slug-collision under any common parent, cycle via
  ancestor-closure DFS capped at 64 hops to mirror the trigger).
- Eight kind constants exported: missing-required, invalid-slug-format,
  invalid-status, slug-collision, cycle, self-parent, unknown-parent,
  unresolvable-path.

Tests cover every kind on both happy and reject paths: missing /
whitespace fields, slug containing dot / upper / whitespace, invalid
status enum, self-parent guard, unknown parent id, root slug collision,
sibling slug collision under common parent, cycle on ancestor closure,
and the "Reader returns ListAll error → validator returns nil" path
(callers see the infra error later, validator doesn't mask it).

No caller migrates yet. Same Go-linker DCE caveat as 5a/5b slice A:
`strings <binary> | grep internal/itemwrite` returns 0 until slice B
imports.

Task: t-projax-5c-itemwrite
2026-05-22 00:33:54 +02:00
..