feat(npm): package_json schema validator (S11 Step 5)
📦 What
Step 5 of the npm local plan, sliced into four stacked ~500-LOC MRs. Slice 1/4 (base) — the package_json schema validator.
New internal/datastore/schemas/npm_package_json.go — ValidateNpmPackageJSON: a //go:embed-ed strict JSON Schema (additionalProperties: false over the spec's allow-list, plus required: ["name", "version"] and "type": "string" on name/version/dist.tarball) compiled once at init, with a 20 KB (20000-byte, SI) ceiling. This is the authoritative package_json gate per ADR 007 (app-layer validation, since CloudSQL disallows the pg_jsonschema DB CHECK the original Step 2 draft used). The version-write slice (3/4) calls this before every npm_versions.package_json write.
✅ Testing
internal/datastore/schemas/npm_package_json_test.go (unit): allow-listed accept; denylist (scripts / preinstall / readme), unknown-field, non-string name/version/dist.tarball, missing required name/version, and over-20 KB reject; size boundary.
Size: ~462 LOC, ~64% test.
🔍 Reviewer note
The validator rejects an empty / JSON-null package_json (Reading A), which diverges from the merged OCI annotations precedent (returns nil for empty). Chosen for consistency with the NOT NULL column and the "authoritative gate" framing; the unit test documents it as PROVISIONAL pending a spec-author nod.
Reviewer follow-ups (addressed in-thread): name/version/dist.tarball are now type-constrained to strings — the schema previously allow-listed field names but enforced no type, so {"name":{...},"version":[]} validated. And the size ceiling was reconciled from 20480 to 20000 to match npm.max_package_json_size (SI "20KB") and the Step 16 handler filter. The remaining AppSec value-shape items — bin/man/directories path-traversal and dist.tarball as format: uri — stay a spec-author follow-up, as they change the allow-list contract beyond a type tightening.
🔗 Stack
Step 5 was split into four stacked MRs (each targets the previous; GitLab auto-retargets to main as they merge), rebased onto the reorganized Step 4b:
- !499 (merged) —
package_jsonschema validator (base → Step 4b) - !503 (merged) — package & file writes + shared test harness
- !504 (merged) — version writes
- !505 (merged) — dist-tag writes
The metadata-cache writes that originally shared slice 4 were carved out: the Step 4b reorg removed the npm_metadata_files read layer they build on (deferred to a later step), so they follow it there.
Related to #122 (closed)