Loading
feat(config): NpmConfig proto + config loader (S11 npm local Step 3)
📦 Summary
Implements Step 3 of the npm local plan (docs/plans/2026-05-11-npm-local.md) — the NpmConfig proto message plus the npm-specific config-loader extension (S11). It extends the existing internal/config loader (already wired into assembleApp); it does not build a new loader or touch the startup path.
Part of #22 (closed) (S11 npm local). Satisfies AC 62.
✨ What changed
- Add
NpmConfigtoproto/artifactregistry/config/v1/config.proto(9 spec-defined fields plus the interimstreampub_buffer_sizeconsumed by the Step 13 streampub library) and regenerate the Go bindings.Configtag 3 stays reserved. - Surface
NpmConfigon the typedConfig. The npm block and every field are optional and defaulted at startup innpmFromProto, followingdocs/dev/configuration.md("the application is responsible for applying defaults at startup") and the existinglogFromProtopattern: an omitted block or field receives its documented default. Per-field protovalidate constraints are therefore intentionally absent (an empty/zero value means "use the default"); provided values are still parsed, and the two cross-field invariants — the 20 KBmax_package_json_sizeceiling (matching the Step 2npm_versionsCHECK) andmax_envelope_dist_tags <= max_tags_per_package— are enforced Go-side with startup errors naming the offending field(s). - Extend the shared size parser to accept decimal-prefixed sizes (the
6.7GBmax_publish_envelope_sizedefault) with exact integer arithmetic, and harden it against a negative-zero integer part and fractional overflow. - Add the
npm:block toconfig.example.yaml(exercised end-to-end by theconfig.Loadguard test).
The generic JSON-safety bounds (max_envelope_depth, max_envelope_object_keys) are intentionally not on NpmConfig: per the S11 spec they are platform-level shared streaming-decoder config (work item #90).
📝 Spec & plan reconciliation (work item #95 drift policy)
Two doc reconciliations ship in this MR (per the #95 "drift corrections land in the implementing MR" policy):
- Plan: Step 3's scope misread !235 (merged) — it listed the depth/key bounds as
NpmConfigfields, but !235 (merged) amended the spec to place them at the platform level (#90). Corrected in adocs(plan):commit, with theFiles:entry reconciled to the actual diff. - Spec (S11): the Configuration proto block defined every npm field with
min_len/gt. To apply defaults perconfiguration.md, those per-field constraints are dropped and the Go-side defaulting documented — amended here so spec, proto, and code stay in sync.
✅ Spec coverage (AC 62)
| Item | Behavior | Tests |
|---|---|---|
| AC 62 | Config loads from YAML via LabKit v2; an unparseable value in any npm size or duration field -> startup error naming that field | TestLoad_Npm_InvalidSizeFields, TestLoad_Npm_InvalidDurationFields (table-driven over every field) |
| AC 62 (per-step) | Valid YAML round-trips; every NpmConfig field asserted (sizes, durations, counters, envelope fields, streampub_buffer_size) |
TestLoad_Npm_HappyPath |
| Defaulting | Omitted npm block -> all documented defaults; partial block -> omitted fields defaulted | TestLoad_Npm_AbsentBlockDefaults, TestLoad_Npm_PartialBlockDefaultsOmittedFields |
| Step 3 | max_package_json_size > 20KB -> startup error naming the field |
TestLoad_Npm_PackageJSONSizeCeiling |
| Step 3 | max_envelope_dist_tags > max_tags_per_package -> error naming both fields; equal values accepted |
TestLoad_Npm_EnvelopeDistTagsInvariant (both subtests) |
| Size parser | Decimal, zero-fraction, non-whole-fraction, malformed, negative-zero, 19-digit-fraction, fractional-sum overflow, numerator*multiplier overflow, negative fractional digits | TestParseSize |
🧪 Testing
- Unit:
internal/config— round-trip, defaulting (absent + partial block), per-field invalid duration/size (table-driven), the 20 KB ceiling, the cross-field invariant (incl. the inclusive boundary), and decimal/fractional/edge-case size parsing.npm.goandsize.goare at 100% statement coverage. config.example.yamlis validated through the realconfig.Loadby the existing guard test.go build ./...clean;go test ./...green.
ℹ️ Notes for review
- Test-first authorship: the branch opens
test(config)->feat(config)->refactor(...), then hardening, reconciliation, and a final coverage + comment-tightening pair (test(config)+docs(config)). Exactly one--no-verifycommit (thetest(config)panic-skeleton, per the documented test-first carve-out); every later commit ran the full hook chain. - Step 13's plan scope still references
npm.max_envelope_depth/npm.max_envelope_object_keys; correcting those depends on the #90 platform-decoder design and is left to Step 13 / #90. ServerConfigdocuments per-field defaults in comments but requires the fields (it does not apply defaults); aligning it withconfiguration.mdis out of scope here.
Edited by David Fernandez