feat(npm): publish Step 4 coherence check (S11 Step 15, 3/3)
📦 What
S11 Step 15 (publish handler - stream + coherence + CAS) ships as a three-MR stack, in dependency order:
- !695 (merged) - streampub CAS sink + attachment gate (library seam).
- !696 (merged) - publish handler streaming/CAS staging (Spec Step 3).
- !697 (merged) - publish Step 4 coherence check (shasum / integrity / manifest). <- this MR
Both parents (!695 (merged), !696 (merged)) have merged, so this MR now targets main (rebased onto it). As the final slice it completes Step 15 in the npm-hosted plan (amends the Step 15 design with the gate / pre-check-before-stream note).
Slice 3 - the Spec Step 4 coherence check on top of the staging stage. After the upload session commits, stagePublish validates the staged tarball against the envelope, all from the in-memory values streampub captured during the pass (no CAS read):
- per-version
versions["{ver}"].name/.version== envelope top-level name + versions-map key, else 422manifest_coherence_failed. - tarball
package.jsonname/version == envelope, else 422manifest_coherence_failed. - SHA-1 ==
versions["{ver}"].dist.shasum, else 422shasum_mismatch. - SHA-512 ==
versions["{ver}"].dist.integritywhen present, matched by token so a multi-hash SRI whose sha512 component is correct still passes; a wrong sha512 is 422integrity_mismatch, while a non-sha512 (unverifiable) SRI is 422integrity_unverifiable.
Per the spec a coherence failure returns 422 while the committed CAS blob stands as a GC-reclaimable orphan (no attachment row is written), distinct from a Step 3 stream failure (which aborts the session before commit). A malformed or type-mismatched per-version object is also a client 422 (streampub re-serializes it faithfully but does not type-check it).
✅ Spec coverage
| Behaviour | Test |
|---|---|
AC 4 - tarball SHA-1 != dist.shasum -> 422 shasum_mismatch |
TestPublishStreamIntegration_ShasumMismatch |
AC 52 - tarball SHA-512 (SRI) != dist.integrity -> 422 integrity_mismatch |
TestPublishStreamIntegration_IntegrityMismatch |
AC 5 - tarball package.json contradicts the envelope -> 422 manifest_coherence_failed |
TestPublishStreamIntegration_ManifestCoherenceFailed |
⚠️ Known gap - route not yet mounted
Same as !696 (merged): the publish route serves the 501 placeholder until the Step 23 mount (closed via a plan amendment), so the integration tests inject the Resolution directly. With Step 15 complete, a coherent publish stages + commits the blob and returns an interim 200; the npm rows + the spec's 201 publish response land in Step 16. Deliberately out of scope.
🧪 Testing
go test ./internal/format/npm/...- passgolangci-lint run ./internal/format/npm/...- 0 issuesgo vet -tags integration ./internal/format/npm/...- clean- Integration tests (
//go:build integration) drive the handler against the S06PgBlobStoreover testcontainers; run in the merged-results pipeline.
Related to #122 (closed)