feat(npm): swap the publish and unpublish counters onto the buffered pipeline (S22 plan: 17/21)

What this step delivers

npm's publish handler and both unpublish handlers now record their repository counter deltas on the buffered counter pipeline. Neither operation changes. Only what each one emits changes.

  • Publish emits one repository-scoped event, Δartifacts = +1 and Δsize = the tarball's bytes, and one namespace-scoped event, Δcomponents = +1 and Δdedup_size. The namespace size delta is gated on the CAS commit's Deduplicated verdict, so a blob the namespace already holds adds nothing to it. The direct UPDATE that moved repositories.artifacts_count and size_bytes is gone. One direct write stays on that row: the last_updated_at stamp, narrowed to that single column and renamed MarkRepositoryLastUpdated.
  • Whole-package unpublish emits Δartifacts = -(versions removed) and Δsize = 0, after the tombstone transaction commits. It replaces the post-commit DecrementRepositoryArtifactsCount write. The interim DecrementRepositorySizeBytes write beside it is removed rather than swapped, because size_bytes settles when the purger reaps the rows.
  • Single-version unpublish emits Δartifacts = -1 and Δsize = 0. This path had no repository-scoped site before. The emit sits outside afterVersionCommit. The handler skips that call on the arm where the removed version was the package's last active one.
  • Neither unpublish handler emits a namespace-scoped delta. Δcomponents settles at the reap, for the same reason as Δsize.
  • The seam is npm.CounterEmitter in internal/format/npm/counter_emitter.go, an interface the consumer declares and *accounting.Emitter satisfies. wireAccounting builds the process's one emitter, and the format tiers read it off the wiring. An emitter over no buffer is inert by contract, which is what the database-less unit stub gets.
  • SumNpmFileSizesByPackage and its one call go with the size decrement they fed. A whole-package unpublish no longer runs that pre-transaction sum. The deleted doc comment recorded its cost: the join to the hash-partitioned blob_storage_blobs prunes no partition.
  • Three specs, the storage-accounting developer document, and the npm e2e catalog are corrected to match the sites.

Spec coverage

Spec: docs/specs/S22-storage-accounting.md

Acceptance criteria

# Criterion Tests
AC-1 A repo-scoped increment is reflected in repositories.artifacts_count/size_bytes after the next drain tick Step 8's (internal/accounting/chunk_worker_integration_test.go). Exercised end to end from an npm call site by TestPublishEmitIntegration_AllFourCountersMoveAfterDrain, TestUnpublishEmitIntegration_ArtifactsFallsAndFootprintHolds
AC-2 A namespace-scoped increment is reflected in namespace_statistics.components_count/deduplicated_size_bytes after the next drain tick Step 8's. Exercised end to end by TestPublishEmitIntegration_AllFourCountersMoveAfterDrain, TestPublishEmitIntegration_SecondAttachMovesComponentsNotDedupSize
AC-3 Concurrent increments to one scope sum exactly, no lost updates Step 5/8's. Not this MR
AC-4 A scope re-incremented mid-claim is re-marked dirty and not double-counted Step 5/8's. Not this MR
AC-5 A chunk past drain_chunk_stale_timeout bails, re-adds its scopes, meters, and issues no UPDATE Step 8's. Not this MR
AC-6 A chunk failing every attempt re-adds its scopes before the terminal error Step 8's. Not this MR
AC-7 Redis unavailability at increment time does not fail the underlying operation Step 6's (internal/accounting/emit_faults_integration_test.go). Not this MR; the emit seam here returns no error, which is what makes the property reachable
AC-8 Reconciliation clears buffered Redis state before its SQL scan Step 13/14's. Not this MR
AC-9 Reconciliation recomputes with the correct soft-delete visibility on every format Step 11's. Not this MR
AC-10 artifacts_count/components_count recomputes covered as a positive hit per version-type table and (format, kind) Step 11/13's. Not this MR
AC-11 A discrepancy is recorded on the drift histogram before the counter is overwritten Step 13's. Not this MR
AC-12 A crash between HINCRBY and SADD leaves the delta recoverable Step 6's fault-injection suite. Not this MR
AC-13 Hash TTLs refresh on every write; a normally drained scope never expires early Step 5's. Not this MR
AC-14 The S22 migrations apply cleanly and roundtrip through jet Steps 1/2a. Not this MR
AC-15 The blob_storage_blobs triggers keep the by-namespace shadow exactly consistent Step 2b's. Not this MR
AC-16 Every namespace has a zero-valued namespace_statistics row by construction Step 1's. Relied on here: every fixture reads the trigger-created row rather than inserting one
AC-17 npm publish swapped onto the pipeline (four counters, Δdedup_size first-attach gated); Δartifacts decrement on both unpublish handlers, after the tombstone commits, on all three arms; the interim size_bytes decrement removed TestPublishEmitIntegration_AllFourCountersMoveAfterDrain, TestPublishEmitIntegration_SecondAttachMovesComponentsNotDedupSize, TestPublishEmitIntegration_DeltaArgumentsAndSourceFirstOrdering, TestUnpublishEmitIntegration_ArtifactsFallsAndFootprintHolds (three arms), TestUnpublishEmitIntegration_DeltaArgumentsAndSourceFirstOrdering (three arms)
AC-18 OCI emits increments at CompleteUpload, MountBlob, and manifest PUT Step 18's. Not this MR
AC-19 OCI emits decrements at the delete handler, not the deleters Step 18's. Not this MR
AC-20 Maven emits all four increments from a post-commit site; BumpRepoCounters retired Step 19's. Not this MR
AC-21 Repository cascade hard-delete emits through S20-A's purger Gated on #464 (closed). Not this MR
AC-22 Reconciliation fans out one task per namespace candidate Step 14/15's. Not this MR
AC-23 A namespace with no statistics row gets one on its first pass (UPSERT) Step 13/14's. Not this MR
AC-24 In-flight reconciliation tasks never exceed reconciliation_max_in_flight Step 14's. Not this MR
AC-25 npm publish row: the npm_versions row is visible outside the transaction when the pipeline call is made; a rolled-back publish issues no delta TestPublishEmitIntegration_DeltaArgumentsAndSourceFirstOrdering (committed-before probe on both emits), TestPublishEmitIntegration_RolledBackPublishEmitsNothing (forced rollback plus a committed control)
AC-25 npm unpublish, both handlers row: the soft_deleted_at write is committed; a rolled-back unpublish issues no delta TestUnpublishEmitIntegration_DeltaArgumentsAndSourceFirstOrdering (committed-before probe, three arms), TestUnpublishEmitIntegration_RolledBackWholePackageEmitsNothing, TestUnpublishEmitIntegration_StaleRevVersionUnpublishEmitsNothing, TestUnpublishEmitIntegration_FailedVersionTransactionEmitsNothing — each pairing the zero-emission assertion with a committed control. Partial: see "Coverage gaps"
AC-25 OCI's four rows and Maven's row Steps 18 and 19. Not this MR
AC-26 last_reconciled_at stamped only after every repository is written back Step 13/14's. Not this MR
AC-27 The trigger selects only namespaces stale beyond reconciliation_interval Step 15's. Not this MR
AC-28 A namespace with an outstanding task is enqueued at most once Step 15's. Not this MR
AC-29 reconciliation_backlog exposed by a single-writer scrape-time collector Step 16's. Not this MR
AC-30 A chunk whose scope was reconciled after its baseline read skips that scope Step 7/8's. Not this MR
AC-31 counter_dirty_set_size sampled once per tick before SPOP Step 10's. Not this MR
AC-32 Config load rejects each invalid storage-accounting configuration Steps 3/3b. Not this MR
AC-33 The six S22-defined metrics are registered and scraped Steps 8/13/16. Not this MR
AC-34 The reconciliation task's saturation policy is enforced Step 14's. Not this MR
AC-35 A namespace-scoped chunk with no statistics row leaves the scope out and still deletes :flushed Step 8's. Not this MR
AC-36 A failing recovery SADD does not lose the delta Step 8's fault-injection suite. Not this MR
AC-37 Management-API deletes emit once their own writes commit Gated on #313 (closed); deliberately outside the plan's numbered steps. Not this MR
AC-38 A persistently failing reconciliation task leaves its namespace re-enqueueable Step 14/15's. Not this MR

Error cases

# Condition Tests
E-1 Redis unavailable at increment time Step 6's emit_faults_integration_test.go. Not re-asserted here; the emit seam returns no error, so no npm arm can surface one
E-2 Redis unavailable at drain-trigger time Step 10's. Not this MR
E-3 Chunk job's Postgres UPDATE fails Step 8's. Not this MR
E-4 Chunk job's :flushed DEL fails after the UPDATE succeeded Step 8's. Not this MR
E-5 Chunk job exhausts all retry attempts Step 8's. Not this MR
E-6 Recovery SADD itself fails Step 8's. Not this MR
E-7 Chunk dequeued later than drain_chunk_stale_timeout Step 8's. Not this MR
E-8 Worker dies mid-chunk after merging into :flushed Step 8's. Not this MR
E-9 Two chunks run one scope concurrently Step 8's. Not this MR
E-10 Trigger's EnqueueTx fails while the process is alive Step 10's. Not this MR
E-11 Crash between a trigger's SPOP and its EnqueueTx Step 10's. Not this MR
E-12 Assigned repository or namespace row hard-deleted before its chunk drains Step 8's. Not this MR
E-13 Namespace-scoped chunk drains a namespace with no statistics row Step 8's. Not this MR
E-14 Crash between HINCRBY and SADD Step 6's. Not this MR
E-15 Crash between reconciliation's clear and its SET Step 13/14's. Not this MR
E-16 Reconciliation scan races a concurrent increment Step 13/14's. Not this MR
E-17 A chunk and a reconciliation process one scope concurrently Step 7/8's. Not this MR
E-18 Reconciliation finds a discrepancy Step 13's. Not this MR
E-19 Namespace has no statistics row when its reconciliation task runs Step 13/14's. Not this MR
E-20 Reconciliation task fails before its final UPSERT Step 14's. Not this MR
E-21 A namespace can never be reconciled Step 14/15's. Not this MR

No error case in the table is an npm call site's to answer: an emit reports no failure to its caller, so every arm the npm handlers can reach is the success arm. What the handlers do owe is that they emit nothing when their own transaction did not commit, which is criterion 25's zero-emission column above rather than a row here.

Security considerations

# Concern Tests
S-1 Redis keys carry only internal UUIDs, so no key-injection or cross-slot risk Step 5's counterbuf key tests own the grammar. Asserted from this side by the emit-argument cases, which pin that both call sites pass the resolved namespace_id/repository_id and no request-derived value: TestPublishEmitIntegration_DeltaArgumentsAndSourceFirstOrdering, TestUnpublishEmitIntegration_DeltaArgumentsAndSourceFirstOrdering
S-2 Counter values are non-secret but feed billing, so a wrong value has financial impact The exact-delta and unchanged-column assertions are this MR's share: TestUnpublishEmitIntegration_ArtifactsFallsAndFootprintHolds (size_bytes, components_count and deduplicated_size_bytes must not move at a tombstone), TestPublishEmitIntegration_SecondAttachMovesComponentsNotDedupSize (no double-count of a namespace-held blob). Reconciliation and the drift metrics are Steps 11-16's
S-3 Redis and Postgres reuse the existing cache client and datastore pool; no new credential surface No new credential surface in this MR either: the pipeline harness builds its client through redisclient.NewCacheClient and datastore.NewAppClient, the same constructors the composition root uses. Nothing to assert

Coverage gaps

Criterion 25's zero-emission column is covered in full for the whole-package handler, through that deleter's own before-cascade fault hook. For the single-version handler it rests on two adjacent routes. datastore.NpmVersionUnpublishDeleter carries no such hook, so a genuine mid-transaction rollback needs new production code to reach. The first route is a stale {rev}, which the real composer rejects before the transaction opens. The second is a seam that returns the error a rollback surfaces as. Both routes enter the handler's error guard, and the emit sits after that guard. The residue is therefore the deleter's rollback-to-error mapping rather than the emit site.

e2e scenarios

docs/testing/e2e/npm.md gains e2e.npm.lifecycle.repository-storage-counters in three places.

  • The lifecycle table gains the scenario row, at status blocked. It reads the management API after a publish and after an unpublish. It polls until the drain lands, rather than reading straight after the response.
  • The note above that table says what blocks it: the periodic drain that moves the buffered deltas into repositories is what makes the read assertable.
  • The usage-data table gains its row. The scenario emits no event of its own, because the publish and the unpublish it reads after are already listed with their events.

Diff size

The diff is 67 files, 3596 insertions and 805 deletions, which passes the 500 reviewable-LOC line in docs/dev/development-model.md. The numbers come from git diff --numstat between main at af5027a60 and the branch tip 1670350b7.

File group Files Added Removed
internal/format/npm/ production 12 672 203
internal/format/npm/ tests 16 2136 223
internal/datastore/ production 7 179 223
internal/datastore/ tests 8 95 42
cmd/artifact-registry/ production 4 150 46
cmd/artifact-registry/ tests 10 270 26
internal/managementapi/ tests 1 10 0
internal/metrics/ 2 19 19
docs/ 6 45 13
.claude/skills/ 1 20 10

The ten groups sum to 67 files, 3596 added and 805 removed. Tests are 2515 of the 3596 added lines, and two new integration suites are 1279 of those 2515: publish_commit_emit_test.go at 743 lines and unpublish_emit_test.go at 536. A third new test file, internal/format/npm/counter_emit_dispatch_internal_test.go, adds 288 lines. Production Go is 987 added and 463 removed across 23 files.

A split does not help here, for three reasons. Both unpublish handler constructors take the emitter as a required argument. A split by emit site therefore leaves one merge request with a handler it cannot construct. The composition root belongs with the sites, because it is what supplies the emitter they read. And the two suites pin the code they ship with, so a test-only merge request fails against main.

Verification

A hand run of the branch against Postgres, MinIO and a live Redis drove all four paths, and read every delta back out of Redis. A publish of a 189-byte tarball moved the repository hash to 1 artifact and 189 bytes. The namespace hash moved to 1 component and 189 bytes. The same publish left repositories.artifacts_count and size_bytes at 0, which is the point of the swap. A second repository in the same namespace took the full 189 bytes, while the namespace's dedup size held at 189. A whole-package unpublish moved artifacts from 3 to 0 with the size unmoved, and both single-version arms moved Δartifacts by -1 with the size unmoved. No conformance suite applies: neither operation's request or response changes, and this MR adds no protocol behavior.

Metrics: the label values that retire, and the ones the emit path adds

Two metrics lose label values with the direct writes this MR removes. Every value in both label sets is a compile-time constant, so nothing request-derived is involved.

Metric Label Retires Appears
gitlab_artifact_registry_npm_buffered_counter_updates_total column repository_publish_counters, artifacts_count last_updated_at, size_bytes
database_query_duration_seconds name repositories_update_increment_publish_counters, npm_files_select_sum_sizes_by_package repositories_update_mark_last_updated

size_bytes sits on both sides of the first row and is the one value whose meaning changes. It stops labelling the unpublish path's buffered UPDATE and starts labelling the counter-emit dispatcher's lost deltas, which report no result="ok".

Nothing in-tree goes stale. repository_publish_counters and both retired query names no longer appear anywhere in the tree; artifacts_count survives only as a response field and as a reconciliation counter name, neither of which is a metric label value. internal/format/npm/metrics.md bounds the column label rather than enumerating it, and its budget of 10 is still met at the six values npm emits: last_downloaded_at, last_updated_at, npm_package_publish_counters, npm_remote_last_downloaded_at, tags_count and size_bytes. An out-of-tree Grafana panel or alert selecting a retired value reads as "no data" rather than failing, which is why the values are listed here rather than left to be discovered.

The mechanism being replaced was metered per column, and the replacement is metered per lost delta. The retired direct writes went through bufferedUpdate, which meters every dispatch on gitlab_artifact_registry_npm_buffered_counter_updates_total{column,result} with result in {ok, panic, dropped}, so dispatch rate and shed rate were visible per column. dispatchCounterEmit now reports on that same vector rather than on a collector of its own, under one fixed column value of size_bytes: result="dropped" for a dispatch shed at a saturated cap, and result="panic" for a recovered seam panic. Without those two, a shed ran no work, wrote no log line and left the operation answering 2xx, so "is npm losing counter deltas right now" had an answer in neither the logs nor /-/metrics.

Two things the reuse does not give back, both stated at the constant in internal/format/npm/counter_emit_dispatch.go. The path reports no result="ok", because a dispatch whose work ran records nothing, so that column carries no denominator and each result is read as a rate on its own. And one column value covers both arms, because dispatchCounterEmit is handed an opaque closure and never runs it on either metered arm: a shed unpublish retires an artifacts_count movement and no bytes, so a non-zero rate means "the emit path lost at least one delta" rather than a byte total.

The reuse costs no cardinality and adds no metric. Both result values already exist on the vector, and size_bytes is a column value npm's own series carried before this swap. The accounting package is untouched. internal/metrics/ is not: retiring repository_publish_counters and artifacts_count and adding last_updated_at moves npm's column values from seven to six and the four packages' union from eleven to ten, which the budget, the pinned set and their ownership comments record. The vector is npm's own interim buffered-write counter rather than a seventh S22 metric, so the set of six that docs/specs/S22-storage-accounting.md:816 delivers, and criterion 33 pins by name, is unchanged and no spec amendment follows.

Database Review Evidence

Query mode only. Migration mode did not run, because the diff changes no file under internal/datastore/migrations/sql/.

The full evidence, with the rendered SQL, the seed shapes, and the raw plans, is in this note.

  • One query-producing record, out of the 59 changed Go files in the diff that ## Diff size measures: RepositoryStore.MarkRepositoryLastUpdated in internal/datastore/repositories.go. internal/datastore/reconcile_repository.go is a comment-only diff. internal/datastore/npm_files.go only removes SumNpmFileSizesByPackage, so it adds no statement and changes none. That removal retires the only all-partition scan of blob_storage_blobs in the tree.
  • Measurement: the plans come from an ephemeral PostgreSQL 17.10 container with the full goose chain applied. Both plans ran inside BEGIN/ROLLBACK after ANALYZE.
  • Recipe run at 50 repositories: a Seq Scan on repositories_p55, 1 partition of 64, execution 1.444 ms.
  • Supplementary run at 5000 rows: an Index Scan on repositories_p58 using repositories_p58_pkey, 1 partition of 64, execution 0.841 ms. The recipe sizes a write target at 50 rows, and that size cannot show whether an index covers the predicate. The larger run confirms that the primary key on (id, namespace_id) covers both predicate columns. It is supplementary to the recipe rather than part of it.
  • No anomalies and no flags.

Correction to the plan. Line 1260 of docs/plans/2026-08-04-s22-storage-accounting.md names internal/datastore/npm_publish_committer.go as the file this evidence covers. NpmPublishCommitter.MarkRepositoryLastUpdated delegates to RepositoryStore, and the jet chain is in internal/datastore/repositories.go. Guardrail 4 keeps a step merge request out of the plan file, so this merge request does not correct it. The section The plan's Status row hands the correction over together with step 17's Status row.

The repository size delta at publish is not first-attach gated

docs/adr/007_database_schema.md line 2092 states that the counter "increments only when a sha256 first becomes attached in the repository". npm's publish site does not implement that gate. emitPublishCounters emits the whole tarball's bytes on every publish, with no test of whether the repository already references that sha256. A repeat publish of byte-identical content into one repository therefore over-counts repositories.size_bytes. What takes the excess back off is a reconciliation pass recomputing the column from the repository's distinct blobs: the over-count stands until a pass reaches the repository, and the column returns to the recomputed value when one does. The direct UPDATE this MR replaces carried the same arithmetic, so the branch moves the gap onto the pipeline rather than introducing it.

This deviation was escalated and accepted. Work item #762 tracks it, together with the packument bytes that no npm emit site carries, on the repository column and on the namespace column. That work item also records the handbook ADR amendment as one of its own deliverables. This MR opens no handbook merge request, and it edits nothing under docs/adr/.

The developer document and the spec disagree in the interim

docs/dev/storage-accounting.md records npm publish's ungated repository size delta here, as an accepted exception to a rule that stands, and points the reader at #762. docs/specs/S22-storage-accounting.md:266 still reads as if every emit site gates, so the two documents disagree while the spec correction is outstanding. That correction is deliberately not in this MR. It belongs to a separate spec merge request, which is not scheduled by this run. Its scope is line 266's accepted-transient paragraph, extended with the sequential repeat attach inside one repository. It states the same in acceptance criterion 17, at line 1013. It leaves line 244 alone, and it leaves line 266's definition of dedup-within-repository alone. No merge order is owed between that merge request and this one.

The plan's Status row

This MR does not edit docs/plans/2026-08-04-s22-storage-accounting.md, and step 17's Status row is empty on main today. Guardrail 4 keeps a step MR out of the plan file, Status table included, and gives that table one writer: a batch or standing docs(plans) merge request, filled in the same sitting the step MR opens. No S22 batch or standing docs(plans) merge request exists today, so the row is handed to a vehicle that does not exist yet. This is a deviation from that guardrail rather than compliance with it. The row is recorded as owed and handed over, and a second correction to the same plan file travels with it: line 1260 points ## Database Review Evidence at internal/datastore/npm_publish_committer.go, where the statement that changed shape is in internal/datastore/repositories.go.

Notes for review

  • The plan's buffered.go bullet is stale, and the file is in this diff for another reason. The plan asks step 17 to correct a TODO(buffered) reference that names "S12 OQ-16 / S23 storage accounting". Merged commit c4189ecc3 made that correction on 2026-08-14, and neither string exists under internal/format/npm/ on main. internal/format/npm/buffered.go is here because its bufferedUpdate godoc named colRepositoryPublishCounters, a constant this MR deletes.
  • The plan's Files list names no composition-root path, and 13 of the 56 changed paths sit under cmd/artifact-registry/. Both unpublish handler constructors take the emitter as a required argument, so the tier that mounts them had to change. This is a gap in the plan's file list rather than scope beyond the step.
  • One retired symbol is named without a file path, on purpose. docs/specs/S22-storage-accounting.md:26 gives the path internal/format/npm/buffered.go for the bufferedUpdate helper, which still exists, and gives no path for IncrementRepositoryPublishCounters, which this MR deletes. A path is a claim that the symbol lives there, and a deleted symbol lives at no path. The asymmetry on that line is deliberate, not an oversight.
  • This MR falsifies a paragraph of another workstream's plan, and a step MR cannot carry the correction. docs/plans/2026-08-12-s17-phase8-statistics.md:62 describes NpmFileStore.SumNpmFileSizesByPackage as present in the tree and tells the Phase 8 author to leave it and SumDistinctNpmFileSizesByVersion alone, "a deliberate near-miss pair". This MR deletes SumNpmFileSizesByPackage, so once it merges there is no pair and the instruction names an object that is not in the tree. The same sentence routes ownership of both sums to work item #549 (closed), which closed on 2026-08-19. docs/plans/README.md:7 reserves a substantive plan revision for a plan-amendment merge request rather than an in-place edit from a step branch, so this MR does not touch that file either. !1786 (merged) is the plan-amendment merge request for that file, and a factual note recording both stale clauses is on it at note 3712392542. Whether its author folds the correction in or not, the paragraph is falsified the moment this MR merges, and this bullet is where that stays on the record.
  • Merge history. This step was developed on top of step 14 while !1757 (merged) was open. !1757 (merged) merged on 2026-08-20 as fceabf70d, and this branch was then rebased onto main.
  • Merge order. This merge request must merge after !1751 (merged) (S22 step 10). The target branch enforces this order, because this merge request targets the branch of !1751 (merged) and not main. !1751 (merged) supplies the drain trigger that applies the deltas this merge request emits. This merge request retires the direct npm writes to repositories.artifacts_count and repositories.size_bytes. No other production path writes either column for an npm repository. Without the drain trigger, the deltas expire on a sliding TTL of drain_key_ttl_multiplier * drain_interval, which is 5 minutes at the default values. These deltas are then lost, and not deferred to a later drain. Reconciliation repairs drift in these columns, and step 15 dispatches it. This merge order does not depend on reconciliation. The retarget is temporary. The target branch returns to main after !1751 (merged) merges, and that change is still owed. While the target is the branch of !1751 (merged), the "Changes" view also shows the commits of !1751 (merged). This is expected, and not extra scope.

Review threads declined

One review thread on this merge request is declined rather than answered. This section records the decision, so a later pass does not re-open it.

The operator declines the confidential AppSec review thread on this merge request. That thread gets no reply, no resolve, and no calibration label.

Review feedback worked after the first review pass

Two findings from the review of 2026-08-21 changed the branch after the description above was first written, and both are recorded here so a reader of the diff is not surprised by them.

  • The single-version unpublish emit is gated on its own committed result. unpublishVersionTx re-reads the target with ActiveNpmVersionExistsTx after the packument rotation, and UnpublishVersion returns NpmVersionUnpublishOutcome rather than four positional values. Two concurrent unpublishes of one version both answer 200, and exactly one of them moves artifacts_count. Acceptance criterion 17 stated the pre-gate premise and is corrected in this merge request; the plan's copy of it is not, because guardrail 4 keeps a step merge request out of the plan file.
  • docs/dev/observability.md gains npm's ..._npm_buffered_counter_updates_total row. npm was the only one of the four packages declaring a column label on that counter with no catalog row. The row states the split reading and records that size_bytes carried an ok denominator until this swap moved the repository counters onto the pipeline.

Related to #515

This is a bot message 🤖 — /smurfit

Edited by Pawel Rozlach

Merge request reports

Loading
Loading