cross-tab-worker-databus 0.21.53 → 0.21.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,39 @@
1
+ ## [0.21.55] - 2026-10-25
2
+
3
+ The release checklist told a reader to re-derive the publish-lag series and, in the same breath, printed the count that re-derivation returns — "42 of its 45 measurements land inside 300–310 s" — in present tense, while its own neighbouring paragraph says that set gains a member at every release. The number had already been overtaken (the sweep now reports the in-band count from a denominator of 63, not 45), so the published checklist was pointing at a stale reading. Documentation only; no behavior, API shape, export, default, frame or storage key changed, and the coverage floors and the zero-count arm ledger are `0.21.54`'s.
4
+
5
+ ### Corrected
6
+
7
+ - **The copied count became a pointer.** Both release checklists now send the reader to the sweep's own `verify inside 300-310s:` line and say the number on it is deliberately not quoted, because a count copied out of a series that grows every release is stale the moment it is written — the `42 … of … 45` this sentence carried had already been overtaken by the time it was re-read. What is stable and stays in prose is the *structure*, re-checked against a live run rather than asserted from the sentence: `0.21.30`'s 364 s budget exhaustion is the only outlier above the band, the two 1.0 s rows are `0.20.86`/`0.20.87` — the only pre-`0.20.92` releases the sweep measures at all — and the other four of that window print as `NEGATIVE`.
8
+
9
+ ### Added
10
+
11
+ - **A gate over the pointer.** A documentation case now walks from each shipped checklist *back to the scripts*: every backticked label the procedure cites must still be printed by some tool, so renaming `verify inside 300-310s:` or `modern floor:` in the code cannot leave the published instructions pointing at output nothing produces. Floors on both sides keep an empty extraction from passing vacuously.
12
+
13
+ ### Verification
14
+
15
+ - `pnpm check` — 40 files and **1042 tests**, plus the 5 perf gates; `pnpm lint`; `pnpm typecheck`; the documentation gate at 28 in both languages, and the new label-pointer case verified in both directions by mutants (rename a label in the tool, strip the citations from a mirror) each with the inserted-diff count asserted first.
16
+ - The band structure above was confirmed against the captured live run's four `NEGATIVE` lines and its `verify inside 300-310s: 60 of 63` line — the counting that Phase 297 had to correct was done from the tool's output, not from the sentence.
17
+
18
+ ## [0.21.54] - 2026-10-24
19
+
20
+ The release checklist tells its reader to take two browser benchmark samples and re-derive a lag series at release step 4, and until now it never said what those tools' failure words mean. Two of them — `NOT ARCHIVED` and `NOT TRUSTED` — describe opposite states of the one artifact the loaded-host rule protects, and a reader who cannot tell them apart does exactly the wrong thing. Documentation only; no behavior, API shape, export, default, frame or storage key changed, and the coverage floors and the zero-count arm ledger are `0.21.51`'s.
21
+
22
+ ### Documentation
23
+
24
+ - **The browser-bench verdict vocabulary, in both languages**, keyed on what each verdict did to `bench-results/` rather than glossed: `archived for trend comparison` means the sample **is** now part of the baseline the next five comparisons are judged against; `NOT ARCHIVED` fires *before* the write, so the archive is unchanged and the numbers printed are diagnosis; `NOT TRUSTED` fires *after* it, so the offending report is the baseline until someone moves it out — which is the difference between re-running and removing, and the reason the rule says to move suspect samples aside; `failed to archive results` is a write failure deliberately not treated as a refusal; and four harness self-checks (`Timed out waiting for`, the dispatch, batch and dedup `mis-measured` cases at targets of 1000, 1000 and 500) mean the measurement never completed at all, fired before the report is built, so there is nothing to excuse or defer and no sample to move.
25
+ - **The lag sweep's seven verdict labels** were named in the previous release; this one completes the pair of tools a release actually runs.
26
+
27
+ ### Fixed
28
+
29
+ - **Four user-facing messages that reached readers with no marker at all.** `bench-browser.mjs` delivered its timeout and three self-check failures through `throw new Error` without the `[bench]` prefix, so they appeared in no shipped documentation and were invisible to the new gate that claims to enumerate the vocabulary. They now carry the prefix, following the convention `verify-release-version.mjs` already sets with `throw new Error('[release] …')`: a user-facing string carries its prefix wherever it is delivered. A probe that throws and is caught two lines later (`'unhealthy'`, the demo-server check) deliberately stays unmarked — it is control flow, never a verdict anyone reads.
30
+
31
+ ### Verification
32
+
33
+ - `pnpm check` — 40 files and **1040 tests**, plus the 5 perf gates; `pnpm lint`; `tests/documentation.test.ts` at 27 in both languages.
34
+ - The vocabulary gate's set is **derived from the scripts**, not restated: it reads both delivery channels and its floor moved 8 → 12 so that a channel going blind is a failure rather than a pass. Two mutants, each with the inserted-diff count asserted before the run — a new `console` verdict (`applied=1`) and a renamed *thrown* one (`applied=2`) — each fail the named case; the second is the proof the new channel feeds the set.
35
+ - Every line number, matrix target and before-or-after-the-write claim above was read off the source and the archive path this cycle, not carried from a previous reading.
36
+
1
37
  ## [0.21.53] - 2026-10-24
2
38
 
3
39
  The release checklist tells the reader to re-derive the publish-lag series after cutting a release. Until now it never said what the tool's failure words mean, and two of the seven it can print read as "your release did not publish" when the instrument is actually saying "this read is not usable". Documentation only; no behavior, API shape, export, default, frame or storage key changed, and the coverage floors and the zero-count arm ledger are `0.21.51`'s.
@@ -7,7 +7,7 @@ import {
7
7
  publicationMetadata,
8
8
  selectWorkerBackend,
9
9
  serializeError
10
- } from "./chunk-PCBQYYOA.js";
10
+ } from "./chunk-TJ7XHT7I.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -2548,7 +2548,7 @@ var SDK_VERSION = (
2548
2548
  // declaration's type is `string | undefined` and the export's is `string`
2549
2549
  // (dropping the conditional is TS2322, measured). Recorded here so the leg reads
2550
2550
  // as a shape the harness cannot reach rather than as a case nobody exercised.
2551
- true ? "0.21.53" : ""
2551
+ true ? "0.21.55" : ""
2552
2552
  );
2553
2553
 
2554
2554
  // src/core/data-bus.ts
@@ -3801,4 +3801,4 @@ export {
3801
3801
  parseDataBusPublication,
3802
3802
  selectWorkerBackend
3803
3803
  };
3804
- //# sourceMappingURL=chunk-PCBQYYOA.js.map
3804
+ //# sourceMappingURL=chunk-TJ7XHT7I.js.map
@@ -2677,7 +2677,7 @@ var SDK_VERSION = (
2677
2677
  // declaration's type is `string | undefined` and the export's is `string`
2678
2678
  // (dropping the conditional is TS2322, measured). Recorded here so the leg reads
2679
2679
  // as a shape the harness cannot reach rather than as a case nobody exercised.
2680
- true ? "0.21.53" : ""
2680
+ true ? "0.21.55" : ""
2681
2681
  );
2682
2682
 
2683
2683
  // src/core/data-bus.ts
@@ -2643,7 +2643,7 @@ var SDK_VERSION = (
2643
2643
  // declaration's type is `string | undefined` and the export's is `string`
2644
2644
  // (dropping the conditional is TS2322, measured). Recorded here so the leg reads
2645
2645
  // as a shape the harness cannot reach rather than as a case nobody exercised.
2646
- true ? "0.21.53" : ""
2646
+ true ? "0.21.55" : ""
2647
2647
  );
2648
2648
 
2649
2649
  // src/core/data-bus.ts
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  selectRebalanceTarget,
21
21
  selectWorkerBackend,
22
22
  topicMatchesPattern
23
- } from "./chunk-PCBQYYOA.js";
23
+ } from "./chunk-TJ7XHT7I.js";
24
24
  import {
25
25
  DEFAULT_STORAGE_PREFIX,
26
26
  PRUNE_STRATEGY,
@@ -25,7 +25,17 @@ Only `pnpm bench:browser` / `pnpm bench:compare` / `pnpm bench:trend` stay local
25
25
  - **With a recorded baseline**, a reading above four times the baseline's own median is refused. The threshold is a ratio rather than an absolute number because the archive spans machines and a load average means nothing without its core count.
26
26
  - **With no recorded baseline** — which is the archive's entire current state, since every report predates the field — a reading above the host's own core count is refused, because more runnable work than cores is oversubscribed by definition and needs no history to establish. Without this leg the guard is weakest exactly where the archive is weakest, and the first sample taken on a busy host is the one that would end a twenty-five-tag deferral streak.
27
27
 
28
- Both boundaries are inclusive and both are a refusal to *trust* rather than a claim the sample is invalid, so a false positive costs one deferred run rather than a release. A failed *write* is deliberately not treated as a refusal: the numbers were produced, a full disk is not a judgment about the host, and conflating the two would fail a release over a sample that was correctly taken. A platform that publishes neither a finite reading nor a core count admits the sample, since there is nothing to judge it on.
28
+ Both boundaries are inclusive and both are a refusal to *trust* rather than a claim the sample is invalid, so a false positive costs one deferred run rather than a release.
29
+
30
+ **Which verdict entered the archive is the fact a reader needs, and the labels say it in words no procedure used to name.** Eight are printed across the two steps, and three of them are exit 1 for reasons that are *not* a performance regression:
31
+
32
+ - `archived for trend comparison` — the sample is in `bench-results/`, so it is part of the baseline the next five comparisons are judged against. This is the sentence the whole loaded-host rule is about, and it is the only one that says it happened.
33
+ - `NOT ARCHIVED: <reason>` (from `bench:browser`, exit 1) — the host-load refusal fired *before* the write, so it prints `the numbers above are printed for diagnosis; re-run on an idle host` and the archive is unchanged: the numbers above it are diagnosis, not a member. A refusal printed about a file that does not exist is a real refusal.
34
+ - `NOT TRUSTED: <reason>` (from `bench:compare`, exit 1) — the opposite state: the sample **is** on disk and the judge is declining to believe it, printing `the numbers below are printed for diagnosis; re-run on an idle host`. This one needs acting on, not just re-running: the offending report is the baseline until it is moved out of `bench-results/`, which is why the rule above says to move suspect samples aside rather than re-run them into place.
35
+ - `failed to archive results: <reason>` (warn, exit 0) — a write failure, deliberately *not* treated as a refusal: the numbers were produced and a full disk says nothing about the host.
36
+ - `no metric in <the newer|newest report> has a baseline sample, so nothing was compared — check that the metric labels still line up` (exit 1) — zero metrics were compared. It is not a pass and not a regression: it means the metric names in the report and the tool have drifted apart.
37
+ - `FAIL: <n>% regression threshold exceeded`, `OK: no metric regressed more than <n>`, and `within-baseline, not gated:` — the three comparison verdicts, where the last lists the metrics the ceiling leg excused rather than letting them pass silently (see above: an empty list is the norm on a quiet host, not a clean bill).
38
+ - `Timed out waiting for <url>`, `wildcard dispatch mis-measured:`, `publishBatch mis-measured:` and `dedup mis-measured:` (exit 1) — a fourth kind, and the one most likely to be mistaken for the first: these are `bench:browser`'s **own self-checks**, fired when the page delivered fewer publications than the harness asked it to (its targets are 1000 dispatched for the two publish matrices and 500 delivered for the dedup matrix), or when the demo server never answered. They mean the *measurement* did not complete, so no report was produced and the archive is untouched — neither a loaded-host refusal nor a regression, and nothing to move aside. Re-run; if it repeats on an idle host, the demo page or the harness changed, which is a code finding rather than a timing one. A failed *write* is deliberately not treated as a refusal: the numbers were produced, a full disk is not a judgment about the host, and conflating the two would fail a release over a sample that was correctly taken. A platform that publishes neither a finite reading nor a core count admits the sample, since there is nothing to judge it on.
29
39
  5. Confirm the package contains only intended files with `npm pack --dry-run --json`.
30
40
  6. Commit on a feature branch, push that branch, and merge its green PR using squash or fast-forward (no merge commit). Fetch the merged commit, tag that exact commit, and push only the specific version tag (with the local tag present, run the gate set once more before that push — see *Tagged-release workflow* below for why that moment is the one no earlier CI sees); never push `main`/`master` directly or force-push. The workflow runs `node scripts/verify-release-version.mjs` to require `RELEASE_TAG` to equal `v` + the package version and to require exactly one non-empty CHANGELOG section. The same step compares every version heading in `CHANGELOG.md` against the registry's own `time` map and refuses a release record that names a version npm never had — but only from `0.20.85` upward, because below that floor 40 sections name versions the registry never recorded, 27 of them tagged (both re-derived 2026-09-25; the command to re-derive them intersects three sets — `grep -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md`, `npm view cross-tab-worker-databus time --json --registry https://registry.npmjs.org`, and `git tag -l` — because the registry read alone answers neither count, let alone the tagged half), and the gate exists to stop a new hole rather than to litigate history. The version being tagged is exempt, since it legitimately cannot be on npm yet, and a registry read that fails prints a skip warning and exits 0: refusing a good release over one transient HTTP call is the failure mode the published-consumer ceiling was sized to avoid, so only a *confirmed* absence is fatal. Note the `--registry` in that re-derive command, and that the gate passes it too: a configured **mirror** can answer successfully while 3,418 s behind. Measured while preparing `0.21.35` on a machine whose `npm` points at `registry.npmmirror.com` — its packument still reported `modified: 00:32:51Z` (the `0.21.33` publish) when the gate ran at `03:41:18Z` — 3,207 s (53 min) after npmjs had recorded `0.21.34` at `02:47:50Z`, and the mirror's own `modified` did not move until `03:44:48.778Z`, 3,418 s after the record — so the gate declared a public version "never recorded", from a read that *succeeded*. A mirror's sync lag is not HTTP caching, so `cache-control: no-cache` returns the same stale document; naming the registry is the only thing that fixes it. `verify-published-consumer.mjs` had already pinned it for both of its reads; this was the last npm-based registry read in the repository that did not.
31
41
 
@@ -53,6 +63,6 @@ For a manual release (no `NPM_TOKEN` in the workflow), run `npm publish --access
53
63
  2. Install the published tarball or version in a clean consumer and import the root plus every public subpath.
54
64
  3. Record the result in the release notes. Do not advance to `1.0.0` until the public API and protocol deprecation policy are explicitly frozen.
55
65
 
56
- The tagged-release workflow already ran the consumer verification above. Run `PUBLISHED_VERSION=<version> pnpm verify:published` by hand only when you need an offline repeat. The workflow's 12-minute ceiling is not padding, and the number is a sum of two measured lags. First: the registry records the publish minutes after `npm publish` returns — and **read that series off the runs rather than off a list in this file**. `node scripts/publication-lag.mjs` walks every release tag, subtracts the `Publish to npm` step's `completed_at` from `time[<version>]` on the packument, prints which attempt each non-first reading came from, and names every tag its denominator drops. **Its verdict vocabulary, because some of the seven labels read as a failed release and are not:** `API-FAIL`, `NO-STEP`, `NO-PUBLISH` and `PENDING-STEP` are all statements about the *read*, not about the publish — `PENDING-STEP <version>` in particular means GitHub has not backfilled the step's `completed_at` yet (it lands after the run row closes; measured on `0.21.52`, whose release printed `NO-PUBLISH` minutes after finishing and the correct row a minute later on the unchanged tree), and the remedy is to re-run rather than to investigate a publish that did happen. `NEGATIVE` is a real reading in another regime (the registry stamped before the ack returned). Only `EXCLUDED-NON-SUCCESS` and `NO-RECORD` name genuine absences, and they name two different ones: a run that did not end green, and a version the registry never recorded. Re-run there at `0.21.36`: **43** releases from `0.20.92` on, record lag **74.8–310.0 s**, none over 310 s. Re-run at `0.21.50`: **57** releases from `0.20.92` on, record lag **56.5–310.0 s**, still none over 310 s — the *low* end is what moved (the 310 s that sizes the budget is the maximum, untouched since `0.21.30`), which is why reading the printed `modern floor:` line is right and copying a floor into prose as a bound is the thing to avoid. This note used to carry the per-version values, and two consecutive releases then refuted the regularity those values had been read as: three records at 76.1 / 75.5 / 75.8 s looked like a stable lag, then `0.21.35` came back at 127.3 s and `0.21.36` at 126.4 s. So the tight run was three samples in a wide distribution, and the enumeration was both a maintenance liability — it needs an edit every release, and it was stale the moment it shipped twice — and the vehicle of an inference the data never supported. The list is gone; the range above is what the ceiling is sized from, and it is re-derivable in one command. What the series does support is weaker and is all the ceiling needs: while the *record* lag spans four minutes, every green release whose consumer step actually waited took **301–309 s** (42 measurements inside 300–310 s, `0.21.30`'s 364 s exhausted budget the only outlier above it, and the two rows below it — `0.20.86` and `0.20.87` at 1.0 s — are the only pre-`0.20.92` releases the sweep measures at all, the other four of that window printing as `NEGATIVE` and so sitting outside the 45) — so the varying quantity is how long the reader stays blind to a record that already exists, which is the second lag, not this one. Second: the packument that `npm pack` resolves against is served with `cache-control: public, max-age=300`, so a copy of it anywhere in the path is allowed to be up to five minutes stale, and resolution cannot be trusted to beat one cache lifetime after a write that lands inside that window. Which copy — npm's local cache or an intermediate — served the stale views below is *not* established, and the counter-observation this note used to offer against a purely local account does not survive being read carefully, because it joins two machines: the 21:44:34Z "fresh fetch" was a `npm pack` run on a laptop, while the 1 s green at 21:47:10Z was a *new runner job*, and a new job's npm cache starts empty — which is what the local account predicts rather than what contradicts it. What a purely-local account still cannot explain is one specific reading: the runner's own `ETARGET` at 21:45:07Z — 364 s after the step that opened its first poll (21:39:03Z), so ~60 s *after* the point where a copy taken then must have gone stale (21:44:03Z, give or take the second the request actually left). The ceiling is therefore arithmetic rather than a model: worst-case ack→`time[]` gap plus one cache lifetime. That sum is not theoretical: the previous 6-minute ceiling was exhausted by a *successful* publish, the 0.21.30 tag run spending its whole 360 s budget (48 × 7.5 s) on `ETARGET` while that version's tarball HEAD and a local `npm pack` had both already succeeded, and the same step 123 s later taking 1 s. The 0.20.89 run had shown the same shape against that release's 2-minute budget. So a long retrying verify is normal propagation, not an unusually slow mirror — raise `PUBLISHED_VERIFY_ATTEMPTS` / `PUBLISHED_VERIFY_DELAY_MS` further only if the ack→`time[]` gap has itself grown. Note the asymmetry when repeating by hand: `verify-published-consumer.mjs` defaults to `6` attempts × `5_000` ms — 30 seconds, not 12 minutes — because the tag run passes the larger pair as env rather than the script owning it. So a manual repeat can report `ETARGET` on a version whose CI run was green, which reads like a failed publish and is not one; reproduce the CI budget explicitly with `PUBLISHED_VERSION=<version> PUBLISHED_VERIFY_ATTEMPTS=96 PUBLISHED_VERIFY_DELAY_MS=7500 pnpm verify:published`, or judge a manual failure against the 30 s budget you actually ran. Read the step's duration as a diagnostic rather than a mood: one 300 s cache lifetime plus a few seconds of command time predicts a tight cluster just above 300 s, and that is what the sweep prints — 42 of its 45 measurements land inside 300–310 s, `0.21.30`'s 364 s budget exhaustion is the only outlier above it, and every sub-2 s row belongs to a release before `0.20.92` where nothing had to propagate. Take the per-release numbers from that run's `Verify published npm consumers` step (`steps[].started_at` / `completed_at`, from the same `gh api` call the attempt selection above uses); this file no longer lists them, because its last enumeration named a 304–308 s band that the whole series widens to 301–309 s, and a number copied out of a subset is how a claim like that survives being false. 0.21.31 through 0.21.34 are the first four releases to run under the raised 720 s ceiling, and their lags split differently, which is the useful contrast: 127.2 s of record plus ~181 s of cache for the first, 76.1 s of record plus ~228 s before the runner saw it for the next, 75.5 s plus ~229 s for the third, and 75.8 s plus ~228 s for the fourth — on 0.21.32 a `no-cache` packument read from a laptop reported the new `latest` 151 s in, while the runner's own poll kept failing until 304 s, i.e. one lifetime after *its* first fetch. So the sample that set the previous ceiling has not been beaten: if the *ack→`time[]`* figure grows past 310 s, raise `PUBLISHED_VERIFY_ATTEMPTS` again; a verify that finishes in seconds means whatever copy it read already carried the new version.
66
+ The tagged-release workflow already ran the consumer verification above. Run `PUBLISHED_VERSION=<version> pnpm verify:published` by hand only when you need an offline repeat. The workflow's 12-minute ceiling is not padding, and the number is a sum of two measured lags. First: the registry records the publish minutes after `npm publish` returns — and **read that series off the runs rather than off a list in this file**. `node scripts/publication-lag.mjs` walks every release tag, subtracts the `Publish to npm` step's `completed_at` from `time[<version>]` on the packument, prints which attempt each non-first reading came from, and names every tag its denominator drops. **Its verdict vocabulary, because some of the seven labels read as a failed release and are not:** `API-FAIL`, `NO-STEP`, `NO-PUBLISH` and `PENDING-STEP` are all statements about the *read*, not about the publish — `PENDING-STEP <version>` in particular means GitHub has not backfilled the step's `completed_at` yet (it lands after the run row closes; measured on `0.21.52`, whose release printed `NO-PUBLISH` minutes after finishing and the correct row a minute later on the unchanged tree), and the remedy is to re-run rather than to investigate a publish that did happen. `NEGATIVE` is a real reading in another regime (the registry stamped before the ack returned). Only `EXCLUDED-NON-SUCCESS` and `NO-RECORD` name genuine absences, and they name two different ones: a run that did not end green, and a version the registry never recorded. Re-run there at `0.21.36`: **43** releases from `0.20.92` on, record lag **74.8–310.0 s**, none over 310 s. Re-run at `0.21.50`: **57** releases from `0.20.92` on, record lag **56.5–310.0 s**, still none over 310 s — the *low* end is what moved (the 310 s that sizes the budget is the maximum, untouched since `0.21.30`), which is why reading the printed `modern floor:` line is right and copying a floor into prose as a bound is the thing to avoid. This note used to carry the per-version values, and two consecutive releases then refuted the regularity those values had been read as: three records at 76.1 / 75.5 / 75.8 s looked like a stable lag, then `0.21.35` came back at 127.3 s and `0.21.36` at 126.4 s. So the tight run was three samples in a wide distribution, and the enumeration was both a maintenance liability — it needs an edit every release, and it was stale the moment it shipped twice — and the vehicle of an inference the data never supported. The list is gone; the range above is what the ceiling is sized from, and it is re-derivable in one command. What the series does support is weaker and is all the ceiling needs: while the *record* lag spans four minutes, every green release whose consumer step actually waited took **301–309 s** (the in-band count is the sweep's own `verify inside 300-310s:` line and is deliberately not copied here; `0.21.30`'s 364 s exhausted budget is the only outlier above the band, and the two rows below it — `0.20.86` and `0.20.87` at 1.0 s — are the only pre-`0.20.92` releases the sweep measures at all, the other four of that window printing as `NEGATIVE` and so outside the in-band count) — so the varying quantity is how long the reader stays blind to a record that already exists, which is the second lag, not this one. Second: the packument that `npm pack` resolves against is served with `cache-control: public, max-age=300`, so a copy of it anywhere in the path is allowed to be up to five minutes stale, and resolution cannot be trusted to beat one cache lifetime after a write that lands inside that window. Which copy — npm's local cache or an intermediate — served the stale views below is *not* established, and the counter-observation this note used to offer against a purely local account does not survive being read carefully, because it joins two machines: the 21:44:34Z "fresh fetch" was a `npm pack` run on a laptop, while the 1 s green at 21:47:10Z was a *new runner job*, and a new job's npm cache starts empty — which is what the local account predicts rather than what contradicts it. What a purely-local account still cannot explain is one specific reading: the runner's own `ETARGET` at 21:45:07Z — 364 s after the step that opened its first poll (21:39:03Z), so ~60 s *after* the point where a copy taken then must have gone stale (21:44:03Z, give or take the second the request actually left). The ceiling is therefore arithmetic rather than a model: worst-case ack→`time[]` gap plus one cache lifetime. That sum is not theoretical: the previous 6-minute ceiling was exhausted by a *successful* publish, the 0.21.30 tag run spending its whole 360 s budget (48 × 7.5 s) on `ETARGET` while that version's tarball HEAD and a local `npm pack` had both already succeeded, and the same step 123 s later taking 1 s. The 0.20.89 run had shown the same shape against that release's 2-minute budget. So a long retrying verify is normal propagation, not an unusually slow mirror — raise `PUBLISHED_VERIFY_ATTEMPTS` / `PUBLISHED_VERIFY_DELAY_MS` further only if the ack→`time[]` gap has itself grown. Note the asymmetry when repeating by hand: `verify-published-consumer.mjs` defaults to `6` attempts × `5_000` ms — 30 seconds, not 12 minutes — because the tag run passes the larger pair as env rather than the script owning it. So a manual repeat can report `ETARGET` on a version whose CI run was green, which reads like a failed publish and is not one; reproduce the CI budget explicitly with `PUBLISHED_VERSION=<version> PUBLISHED_VERIFY_ATTEMPTS=96 PUBLISHED_VERIFY_DELAY_MS=7500 pnpm verify:published`, or judge a manual failure against the 30 s budget you actually ran. Read the step's duration as a diagnostic rather than a mood: one 300 s cache lifetime plus a few seconds of command time predicts a tight cluster just above 300 s, and that is the shape the sweep prints on its own `verify inside 300-310s:` line. The count on that line is **not quoted here**, because this file's other paragraph says the series "gains a member at every release", and a count copied out of it is stale the moment it is written — the one this sentence used to carry, `42 … of … 45`, had already been overtaken by the time it was re-read. What *is* stable and worth stating: `0.21.30`'s 364 s budget exhaustion is the only outlier above the band, and every sub-2 s row belongs to a release before `0.20.92` where nothing had to propagate. Take the per-release numbers from that run's `Verify published npm consumers` step (`steps[].started_at` / `completed_at`, from the same `gh api` call the attempt selection above uses); this file no longer lists them, because its last enumeration named a 304–308 s band that the whole series widens to 301–309 s, and a number copied out of a subset is how a claim like that survives being false. 0.21.31 through 0.21.34 are the first four releases to run under the raised 720 s ceiling, and their lags split differently, which is the useful contrast: 127.2 s of record plus ~181 s of cache for the first, 76.1 s of record plus ~228 s before the runner saw it for the next, 75.5 s plus ~229 s for the third, and 75.8 s plus ~228 s for the fourth — on 0.21.32 a `no-cache` packument read from a laptop reported the new `latest` 151 s in, while the runner's own poll kept failing until 304 s, i.e. one lifetime after *its* first fetch. So the sample that set the previous ceiling has not been beaten: if the *ack→`time[]`* figure grows past 310 s, raise `PUBLISHED_VERIFY_ATTEMPTS` again; a verify that finishes in seconds means whatever copy it read already carried the new version.
57
67
 
58
68
  **Do not adjudicate a publish from `dist-tags.latest`.** The package document endpoint and the tarball URL are two different read paths, and their timestamps are worth more than the impression they first give. Read during the 0.21.30 release, in order: `npm publish` returned success at `21:39:03Z`; a `curl -s -H "Cache-Control: no-cache" https://registry.npmjs.org/<pkg>` at `21:43:15Z` reported `latest` still on the previous version, no `0.21.30` in `versions`, and no `time['0.21.30']`; npm's own stamp for that version is `21:44:12.987Z` and the document's `last-modified` is `21:44:14 GMT`. So the 21:43:15 read was **not** a replica refusing an artifact that already existed — the registry had not finished recording the publish, and `no-cache` could not have made it see a version that did not exist yet. Absence from `versions` and from `latest`, minutes after a green publish step, is the expected state of a successful release from `0.20.92` on — **43** of them as of `0.21.36`, every one of *those* recorded at least 74.8 s after its ack — a floor `0.21.50` then widened down to 56.5 s. The floor is not what the ceiling is built from, so a reading below it costs nothing; only the *upper* bound is load-bearing, and the sweep still prints `ceiling contribution unchanged if max <= 310.0: true`. Take the count from `CHANGELOG.md`'s version headings intersected with the packument's `time` map and the floor from the same sweep that yields the lag range above: that set gains a member at every release, which is the whole reason a value in this sentence is the wrong thing to memorize — while the five just before the window (`0.20.86`, `0.20.87`, `0.20.88`, `0.20.90`, `0.20.91`) recorded within ±0.6 s of their acks and resolved in 1 s. So the sentence describes this window and not npm; what to do when it holds is unchanged. The genuine read-path disagreement came 60 s later: between `21:44:14Z` and `21:44:35Z`, `curl -I …/-/<pkg>-0.21.30.tgz` returned `HTTP 200` and a local `npm pack <pkg>@0.21.30 --registry https://registry.npmjs.org` completed and wrote the tarball, while the workflow runner's own `npm pack` kept returning `ETARGET` until it exhausted its budget at `21:45:07Z`. Same version, two machines, opposite answers a minute apart. To answer "did the publish happen", HEAD the tarball URL and try the pack — those reach the artifact — and try it **from the place that is failing**, because "fetchable" is a per-machine answer; read `latest` only for the question "which version do consumers get by default", which is a different one and settles later. `npm view <pkg> version` adds a third path on top: it can be served from npm's *local* packument cache, so on a machine that queried the registry before the publish it can return the superseded version even after the global `latest` has moved (that is why this gate resolves its target from `PUBLISHED_VERSION` and refuses an inferred version that disagrees with `package.json`). Never re-push, move, or replace a tag because a metadata read looked stale; the tag is immutable once published.
package/docs/roadmap.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Roadmap
2
2
 
3
- 0.21.52 was released on October 24, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
3
+ 0.21.54 was released on October 24, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
4
4
 
5
5
  ## 0.21.44 delivered scope
6
6
 
@@ -26,7 +26,17 @@
26
26
  - **归档中已有可比较的读数时**,超过基线中位数四倍的读数被拒绝。取比值而非绝对值,是因为归档跨机器,而负载均值脱离核心数没有意义。
27
27
  - **归档中没有任何读数时**——这正是当前归档的全部状态,因为每份报告都早于该字段——超过主机自身核心数的读数被拒绝,因为可运行负载超过核心数按定义就是超订,且这不需要任何历史即可成立。没有这一条,守卫恰好在归档最弱的地方也最弱,而第一个在繁忙主机上采到的样本,正是那个会让二十五次发布推迟 streak 结束的样本。
28
28
 
29
- 两条边界都取闭区间,且都是"不予*采信*"而不是"样本无效"的判定,因此误报只代价一次推迟,而不是一次发布。写入*失败*被刻意区别对待、不算拒绝:数字已经产出,磁盘写满不是对主机的判断,把两者混为一谈会让一个采样正确、只是存不下来的样本卡住一次发布。既不发布有限读数也不发布核心数的平台会放行该样本,因为没有任何东西可以据以判定它。
29
+ 两条边界都取闭区间,且都是"不予*采信*"而不是"样本无效"的判定,因此误报只代价一次推迟,而不是一次发布。
30
+
31
+ **「哪条裁决进了归档」才是读者需要的事实,而这些标签用词说出的这件事,此前没有任何流程命名过。** 两个步骤一共打印八个裁决,其中三个是退出码 1,而原因都**不是**性能回归:
32
+
33
+ - `archived for trend comparison` —— 样本已进入 `bench-results/`,于是它就是接下来五次比较所依据的基线的一部分。整条「负载主机」规则说的就是这件事,而这是唯一一句说明它发生了的话。
34
+ - `NOT ARCHIVED: <原因>`(来自 `bench:browser`,退出码 1)—— 宿主负载的拒绝在写入*之前*触发,所以它会打印 `the numbers above are printed for diagnosis; re-run on an idle host`,而归档没有任何变化:上面那些数字只是诊断,不是基线成员。一份不存在的文件上打印的拒绝,才是真正的拒绝。
35
+ - `NOT TRUSTED: <原因>`(来自 `bench:compare`,退出码 1)—— 相反的状态:样本**已经**在磁盘上,是判定方拒绝采信它,并打印 `the numbers below are printed for diagnosis; re-run on an idle host`。这条需要动手处理,而不只是重跑:惹事的那份报告会成为基线,直到它被移出 `bench-results/`——这正是上面的规则说「把可疑样本挪开」而不是「重跑把它冲掉」的原因。
36
+ - `failed to archive results: <原因>`(warn,退出码 0)—— 一次写入失败,故意**不**当作拒绝:数字已经产出,而磁盘满了并不说明宿主的情况。
37
+ - `no metric in <the newer|newest report> has a baseline sample, so nothing was compared — check that the metric labels still line up`(退出码 1)—— 零个指标被比较。它既不是通过也不是回归:它意味着报告里的指标名和工具用的名字已经对不上了。
38
+ - `FAIL: <n>% regression threshold exceeded`、`OK: no metric regressed more than <n>` 与 `within-baseline, not gated:` —— 三条比较裁决,最后一条会列出被百分比上限放过、而不是静默通过的指标(见上文:在空闲主机上「空清单」才是常态,那并不是一张合格证)。
39
+ - `Timed out waiting for <url>`、`wildcard dispatch mis-measured:`、`publishBatch mis-measured:` 与 `dedup mis-measured:`(退出码 1)—— 第四类,也是最容易被误当成第一类的那几种:它们是 `bench:browser` **自己的自检**,在页面实际投递的发布数少于工具要求的目标时触发(两个 publish 矩阵的目标是 1000 次派发,dedup 矩阵是 500 次送达),或者 demo 服务器一直没有应答。它们意味着*测量*没有完成,因此没有产出报告,归档也没有被动过 —— 既不是负载主机的拒绝,也不是回归,更没有东西需要挪开。重跑即可;若在空闲主机上重复出现,那就是 demo 页面或工具本身变了,这是一个代码发现,而不是时序问题。写入*失败*被刻意区别对待、不算拒绝:数字已经产出,磁盘写满不是对主机的判断,把两者混为一谈会让一个采样正确、只是存不下来的样本卡住一次发布。既不发布有限读数也不发布核心数的平台会放行该样本,因为没有任何东西可以据以判定它。
30
40
  5. 用 `npm pack --dry-run --json` 确认发布包只包含预期文件。
31
41
  6. 在功能分支提交并推送该分支,PR 验证通过后使用 squash 或 fast-forward 合入(不创建 merge commit)。获取合入后的精确提交并打 tag,只推送该版本 tag(推送之前,带着本地已存在的 tag 再把门禁跑一遍——原因见下文“打 tag 的发布工作流”,那是此前的 CI 覆盖不到的构建状态);禁止直接推送 `main`/`master` 或 force-push。工作流运行 `node scripts/verify-release-version.mjs`,要求 `RELEASE_TAG` 等于 `v` 加 package 版本,且 CHANGELOG 中恰好有一个非空的对应版本章节。同一步骤还会把 `CHANGELOG.md` 里每个版本标题与 registry 自己的 `time` 映射相对照,拒绝一份"点名了 npm 从未有过的版本"的发布记录——但只从 `0.20.85` 往上要求:在那之下有 40 个章节点名了 registry 从未记录过的版本,其中 27 个还带着 git tag(两个数都在 2026-09-25 重新核过;重算的命令要把三个集合求交——`grep -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md`、`npm view cross-tab-worker-databus time --json --registry https://registry.npmjs.org`、`git tag -l`——因为只读 registry 既得不出 40,更得不出其中带 tag 的 27),这道门禁要拦的是新的漏洞,不是去翻历史旧账。而 registry 读取失败时,这一半会被跳过并打印一行警告、退出 0,而不是把一次好发布判红:为一个瞬时的 HTTP 失败拒绝一次成功发布,正是已发布消费者上限当初要避免的那类失败,所以只有*确认存在*的缺失才会中止发布。请注意上面那条重算命令里的 `--registry`,也请注意门禁自己也会带上它:一台配置了**镜像**的机器可以成功应答,却落后 3,418 秒。这是准备 `0.21.35` 时在本机量到的——这台机器的 `npm` 指向 `registry.npmmirror.com`,它的 packument 仍报 `modified: 00:32:51Z`(也就是 `0.21.33` 那次发布),而门禁运行在 `03:41:18Z`——比 npmjs 在 `02:47:50Z` 记下 `0.21.34` 晚 3,207 秒(53 分钟)——镜像自己的 `modified` 直到 `03:44:48.778Z` 才动,比那条记录晚 3,418 秒,于是门禁把一个已经公开的版本判成"registry 从未记录",而这一次读取是**成功**的。镜像的同步滞后不是 HTTP 缓存,所以 `cache-control: no-cache` 拿回来的还是同一份旧文档;只有显式点名 registry 才能纠正。`verify-published-consumer.mjs` 早已为它的两次读取钉住了 registry,这里是仓库里最后一处没有钉的 npm 读取。正在打 tag 的那个版本本身被豁免,因为它此刻本来就不该在 npm 上。
32
42
 
@@ -54,6 +64,6 @@
54
64
  2. 在干净消费者中安装已发布版本或 tarball,并导入主入口及所有公开子路径。
55
65
  3. 将结果记录到发布说明。在公开 API 和协议弃用策略明确冻结前,不进入 `1.0.0`。
56
66
 
57
- 打 tag 的发布工作流已自动执行上述消费者验证;仅在需要离线复验时才手动运行 `PUBLISHED_VERSION=<version> pnpm verify:published`。工作流的 12 分钟上限不是凑数,而是两段实测延迟之和。第一段:`npm publish` 返回成功后,registry 还要几分钟才把这条版本记进去——而**这份序列要从运行记录里读,不要从本文件抄来的清单里读**。`node scripts/publication-lag.mjs` 会走一遍所有 release tag,用 packument 上的 `time[<version>]` 减去 `Publish to npm` 步骤的 `completed_at`,打印每条非首次读出的数取自第几次 attempt,并把它从分母里丢掉的 tag 逐个点名。**它的裁决词表,因为七个标签里有几个会被读成「发布失败」而其实不是:** `API-FAIL`、`NO-STEP`、`NO-PUBLISH`、`PENDING-STEP` 说的都是*这次读取*,而不是这次发布——尤其 `PENDING-STEP <version>` 表示 GitHub 还没有回填该步骤的 `completed_at`(它在 run 行关闭之后才落地;在 `0.21.52` 上实测:发布刚结束时打印 `NO-PUBLISH`,一分钟之后在未改动的工作树上重跑就是正确的行),处置办法是重跑,而不是去查一次并不存在的发布失败。`NEGATIVE` 是另一区制下的真实读数(registry 在 ack 返回之前就盖了时间戳)。只有 `EXCLUDED-NON-SUCCESS` 与 `NO-RECORD` 指向真正的缺失,而且是两种不同的缺失:一次没有以绿色结束的 run,和一个 registry 从未记录的版本。在 `0.21.36` 处重跑的答案:从 `0.20.92` 起共 **43** 个发布,落库延迟 **74.8–310.0 秒**,没有一次越过决定预算的 310 秒。在 `0.21.50` 处重跑:共 **57** 个发布,落库延迟 **56.5–310.0 秒**,仍然没有一次越过 310 秒——移动的是*下界*(给预算定尺寸的那个 310 秒是自 `0.21.30` 以来的最大值,未被触及),这正是「该读扫描打印的 `modern floor:` 那行,而不是把下限抄进散文当成界」的原因。本段以前逐个抄录过这些数值,而紧接着的两次发布接连推翻由那串数值读出的规律:连续三次落在 76.1 / 75.5 / 75.8 秒,看着像"落库很稳",随后 `0.21.35` 回到 127.3 秒、`0.21.36` 是 126.4 秒。所以那段紧凑只是宽分布里的三个样本;而那份枚举既是维护负担(每次发布都要改,两次刚写下就已过期),也承载了一个数据从来不支持的推论。清单已删,上面这个区间才是预算的来源,而且一条命令就能重取。这套数据真正支持的结论更弱、也更够用:落库延迟横跨四分钟,可绿色发布里真正等待过的消费者步骤一律是 **301–309 秒**(45 个读数里 42 个落在 300–310 秒内,唯一的上方异常是 `0.21.30` 耗尽预算的 364 秒;下方两条各 1.0 秒,是 `0.20.86` 和 `0.20.87`,也正是 `0.20.92` 之前唯二被这条序列量到的发布——那个窗口里另外四条以 `NEGATIVE` 打印,不在这 45 条之内——那时根本没有东西需要传播)——会变的其实是"记录已经存在之后,读者还要瞎多久",那是第二段,不是这一段。第二段:`npm pack` 用来解析版本的 packument 是以 `cache-control: public, max-age=300` 提供的,也就是说路径上任何一份副本按设计都可以旧五分钟,落在这一窗口内写入的版本,解析不太可能快过一整个缓存生命周期。究竟是哪个副本(npm 本地缓存,还是中间的 CDN)在下面提供了过期视图**并未确立**,而这段文字此前用作"反对纯本地解释"的那个观测,仔细读过之后站不住,因为它把两台机器拼成了一句话:21:44:34Z 那次"新鲜读取"是在笔记本上跑 `npm pack`,而 21:47:10Z 一秒就绿的是一次*全新的 runner 任务*,新任务的 npm 缓存本来是空的——这恰好是纯本地模型预测的结果,不是反驳它的结果。纯本地模型仍然解释不了的,只有一个读数:runner 自己在 21:45:07Z 仍然报 `ETARGET`——距离开始轮询的那一步(21:39:03Z)已经 364 秒,也就是比"那一刻抓到的副本必然已过期"的时间点(21:44:03Z,误差在请求真正发出的那一秒之内)还要晚约 60 秒。因此上限靠的是算术而不是模型:最坏情况的"确认 → `time[]`"间隔,加上一个缓存生命周期。这个和并非理论值——上一个 6 分钟上限被一次*成功*的发布排空过:0.21.30 的 tag 运行把整整 360 秒预算(48 × 7.5 秒)花在 `ETARGET` 上,而那时该版本的 tarball HEAD 和本地 `npm pack` 都已经成功;123 秒后同一步骤只用了 1 秒。0.20.89 针对它当时的 2 分钟预算也是同一形状。所以长时间的反复重试是正常的传播延迟,不是镜像异常慢——只有"确认 → `time[]`"间隔本身变长了,才需要继续调大 `PUBLISHED_VERIFY_ATTEMPTS` 和 `PUBLISHED_VERIFY_DELAY_MS`。手动复验时要注意这个不对称:`verify-published-consumer.mjs` 的默认值是 `6` 次 × `5_000` ms——30 秒而不是 12 分钟——因为更大的那一对是由 tag 工作流以 env 传入、而非脚本自带。于是手动复跑可能在一个 CI 全绿的版本上报出 `ETARGET`,这看起来像发布失败而实际不是;要对齐 CI 预算就显式执行 `PUBLISHED_VERSION=<version> PUBLISHED_VERIFY_ATTEMPTS=96 PUBLISHED_VERIFY_DELAY_MS=7500 pnpm verify:published`,否则请按你实际跑到的 30 秒预算来判断手动失败。把这个步骤的耗时当诊断读数而不是情绪指标:整条序列自己取——`node scripts/publication-lag.mjs` 会打印每个发布运行里这一步的墙钟时间(`steps[].started_at`/`completed_at`,和上面选 attempt 用的是同一个 `gh api` 调用)。45 个读数里 42 个落在 300–310 秒内,`0.21.30` 耗尽预算的 364 秒是唯一的上方异常,1 秒以内的两条都属于 `0.20.92` 之前的发布——那时没有东西需要传播。这正是"一个 300 秒缓存生命周期 + 几秒命令时间"会预测出的形状,所以本文件不再抄录逐个数:上一份枚举写下的是 304–308 秒的区间,而整条序列把它放宽成 301–309 秒——从子集里抄一个数,就是这么让一个错说法活得比它的样本更久。0.21.31 到 0.21.34 是头四个跑在提高后的 720 秒上限下的发布,而它们几段延迟的切分方式不同,这个对照正是有用的地方:第一次是 127.2 秒落库 + 约 181 秒缓存;第二次是 76.1 秒落库 + 之后约 228 秒 runner 仍看不见它;第三次是 75.5 秒落库 + 约 229 秒;第四次是 75.8 秒落库 + 约 228 秒——在 0.21.32 上,笔记本在 151 秒时用 `no-cache` 读 packument 已经能看到新的 `latest`,而 runner 自己的轮询一直到 304 秒才成功,也就是从*它自己*首次抓取算起整整一个生命周期。所以当初定这个上限的那个样本至今没有被超过:如果*确认 → `time[]`*这一段自己长过了 310 秒,就再调大 `PUBLISHED_VERIFY_ATTEMPTS`;而几秒就结束的 verify 只说明它读到的那份副本已经带上了新版本。
67
+ 打 tag 的发布工作流已自动执行上述消费者验证;仅在需要离线复验时才手动运行 `PUBLISHED_VERSION=<version> pnpm verify:published`。工作流的 12 分钟上限不是凑数,而是两段实测延迟之和。第一段:`npm publish` 返回成功后,registry 还要几分钟才把这条版本记进去——而**这份序列要从运行记录里读,不要从本文件抄来的清单里读**。`node scripts/publication-lag.mjs` 会走一遍所有 release tag,用 packument 上的 `time[<version>]` 减去 `Publish to npm` 步骤的 `completed_at`,打印每条非首次读出的数取自第几次 attempt,并把它从分母里丢掉的 tag 逐个点名。**它的裁决词表,因为七个标签里有几个会被读成「发布失败」而其实不是:** `API-FAIL`、`NO-STEP`、`NO-PUBLISH`、`PENDING-STEP` 说的都是*这次读取*,而不是这次发布——尤其 `PENDING-STEP <version>` 表示 GitHub 还没有回填该步骤的 `completed_at`(它在 run 行关闭之后才落地;在 `0.21.52` 上实测:发布刚结束时打印 `NO-PUBLISH`,一分钟之后在未改动的工作树上重跑就是正确的行),处置办法是重跑,而不是去查一次并不存在的发布失败。`NEGATIVE` 是另一区制下的真实读数(registry 在 ack 返回之前就盖了时间戳)。只有 `EXCLUDED-NON-SUCCESS` 与 `NO-RECORD` 指向真正的缺失,而且是两种不同的缺失:一次没有以绿色结束的 run,和一个 registry 从未记录的版本。在 `0.21.36` 处重跑的答案:从 `0.20.92` 起共 **43** 个发布,落库延迟 **74.8–310.0 秒**,没有一次越过决定预算的 310 秒。在 `0.21.50` 处重跑:共 **57** 个发布,落库延迟 **56.5–310.0 秒**,仍然没有一次越过 310 秒——移动的是*下界*(给预算定尺寸的那个 310 秒是自 `0.21.30` 以来的最大值,未被触及),这正是「该读扫描打印的 `modern floor:` 那行,而不是把下限抄进散文当成界」的原因。本段以前逐个抄录过这些数值,而紧接着的两次发布接连推翻由那串数值读出的规律:连续三次落在 76.1 / 75.5 / 75.8 秒,看着像"落库很稳",随后 `0.21.35` 回到 127.3 秒、`0.21.36` 是 126.4 秒。所以那段紧凑只是宽分布里的三个样本;而那份枚举既是维护负担(每次发布都要改,两次刚写下就已过期),也承载了一个数据从来不支持的推论。清单已删,上面这个区间才是预算的来源,而且一条命令就能重取。这套数据真正支持的结论更弱、也更够用:落库延迟横跨四分钟,可绿色发布里真正等待过的消费者步骤一律是 **301–309 秒**(带内计数由扫描自己的 `verify inside 300-310s:` 行打印,这里刻意不抄数字;唯一的上方异常是 `0.21.30` 耗尽预算的 364 秒;下方两条各 1.0 秒,是 `0.20.86` 和 `0.20.87`,也正是 `0.20.92` 之前唯二被这条序列量到的发布——那个窗口里另外四条以 `NEGATIVE` 打印,落在带内计数之外——那时根本没有东西需要传播)——会变的其实是"记录已经存在之后,读者还要瞎多久",那是第二段,不是这一段。第二段:`npm pack` 用来解析版本的 packument 是以 `cache-control: public, max-age=300` 提供的,也就是说路径上任何一份副本按设计都可以旧五分钟,落在这一窗口内写入的版本,解析不太可能快过一整个缓存生命周期。究竟是哪个副本(npm 本地缓存,还是中间的 CDN)在下面提供了过期视图**并未确立**,而这段文字此前用作"反对纯本地解释"的那个观测,仔细读过之后站不住,因为它把两台机器拼成了一句话:21:44:34Z 那次"新鲜读取"是在笔记本上跑 `npm pack`,而 21:47:10Z 一秒就绿的是一次*全新的 runner 任务*,新任务的 npm 缓存本来是空的——这恰好是纯本地模型预测的结果,不是反驳它的结果。纯本地模型仍然解释不了的,只有一个读数:runner 自己在 21:45:07Z 仍然报 `ETARGET`——距离开始轮询的那一步(21:39:03Z)已经 364 秒,也就是比"那一刻抓到的副本必然已过期"的时间点(21:44:03Z,误差在请求真正发出的那一秒之内)还要晚约 60 秒。因此上限靠的是算术而不是模型:最坏情况的"确认 → `time[]`"间隔,加上一个缓存生命周期。这个和并非理论值——上一个 6 分钟上限被一次*成功*的发布排空过:0.21.30 的 tag 运行把整整 360 秒预算(48 × 7.5 秒)花在 `ETARGET` 上,而那时该版本的 tarball HEAD 和本地 `npm pack` 都已经成功;123 秒后同一步骤只用了 1 秒。0.20.89 针对它当时的 2 分钟预算也是同一形状。所以长时间的反复重试是正常的传播延迟,不是镜像异常慢——只有"确认 → `time[]`"间隔本身变长了,才需要继续调大 `PUBLISHED_VERIFY_ATTEMPTS` 和 `PUBLISHED_VERIFY_DELAY_MS`。手动复验时要注意这个不对称:`verify-published-consumer.mjs` 的默认值是 `6` 次 × `5_000` ms——30 秒而不是 12 分钟——因为更大的那一对是由 tag 工作流以 env 传入、而非脚本自带。于是手动复跑可能在一个 CI 全绿的版本上报出 `ETARGET`,这看起来像发布失败而实际不是;要对齐 CI 预算就显式执行 `PUBLISHED_VERSION=<version> PUBLISHED_VERIFY_ATTEMPTS=96 PUBLISHED_VERIFY_DELAY_MS=7500 pnpm verify:published`,否则请按你实际跑到的 30 秒预算来判断手动失败。把这个步骤的耗时当诊断读数而不是情绪指标:整条序列自己取——`node scripts/publication-lag.mjs` 会打印每个发布运行里这一步的墙钟时间(`steps[].started_at`/`completed_at`,和上面选 attempt 用的是同一个 `gh api` 调用),并在自己的 `verify inside 300-310s:` 行里打印落在带内的读数占比。**那行上的计数不抄在这里**,因为本文件另一段已经说了这个序列「每次发布都会 +1」,抄下来的数写下即过期——这句话此前抄的 `45 个读数里 42 个`,在被重读时就已经落后于实际。值得留的是稳定的结构:唯一的上方异常是 `0.21.30` 耗尽预算的 364 秒,1 秒以内的行都属于 `0.20.92` 之前的发布——那时没有东西需要传播。这正是"一个 300 秒缓存生命周期 + 几秒命令时间"会预测出的形状,所以本文件不再抄录逐个数:上一份枚举写下的是 304–308 秒的区间,而整条序列把它放宽成 301–309 秒——从子集里抄一个数,就是这么让一个错说法活得比它的样本更久。0.21.31 到 0.21.34 是头四个跑在提高后的 720 秒上限下的发布,而它们几段延迟的切分方式不同,这个对照正是有用的地方:第一次是 127.2 秒落库 + 约 181 秒缓存;第二次是 76.1 秒落库 + 之后约 228 秒 runner 仍看不见它;第三次是 75.5 秒落库 + 约 229 秒;第四次是 75.8 秒落库 + 约 228 秒——在 0.21.32 上,笔记本在 151 秒时用 `no-cache` 读 packument 已经能看到新的 `latest`,而 runner 自己的轮询一直到 304 秒才成功,也就是从*它自己*首次抓取算起整整一个生命周期。所以当初定这个上限的那个样本至今没有被超过:如果*确认 → `time[]`*这一段自己长过了 310 秒,就再调大 `PUBLISHED_VERIFY_ATTEMPTS`;而几秒就结束的 verify 只说明它读到的那份副本已经带上了新版本。
58
68
 
59
69
  **不要用 `dist-tags.latest` 来判断发布是否发生。** 包文档端点与 tarball URL 是两条不同的读取路径,而它们的时间戳比这两条路径给的第一手印象更值得读。0.21.30 发布期间按顺序读到:`npm publish` 在 `21:39:03Z` 返回成功;`21:43:15Z` 的一次 `curl -s -H "Cache-Control: no-cache" https://registry.npmjs.org/<pkg>` 仍报 `latest` 是上一个版本、`versions` 里没有 `0.21.30`、`time['0.21.30']` 也不存在;而 npm 自己给这个版本盖的时间戳是 `21:44:12.987Z`,文档的 `last-modified` 是 `21:44:14 GMT`。所以 21:43:15 那次读取**不是**一份副本在拒绝已经存在的产物——registry 当时确实还没把这次发布记完,`no-cache` 也不可能让它看见尚不存在的版本;在发布步骤全绿之后的几分钟里,`versions` 与 `latest` 里没有这个版本,对 `0.20.92` 起的每一次成功发布都是正常状态——截至 `0.21.36` 共 **43** 个,其中*那四十三次*被记下的延迟都不小于 74.8 秒——这个下限随后被 `0.21.50` 向下放宽到 56.5 秒。下限并不是预算的推导来源,所以它变小并不损失什么;真正承重的是*上界*,而扫描仍然打印 `ceiling contribution unchanged if max <= 310.0: true`。(计数取 `CHANGELOG.md` 的版本标题与 packument `time` 映射的交集,下限由上面那条同一命令给出;这个集合每次发布都会 +1,所以值得记住的是取法而不是那个数)——而窗口之前的那五次(`0.20.86`、`0.20.87`、`0.20.88`、`0.20.90`、`0.20.91`)在 ack 前后 ±0.6 秒内就记录好了,1 秒即可解析。所以这句话描述的是这个窗口,而不是 npm 本身;当它成立时,该怎么判断并没有变。真正的读取路径分歧出现在 60 秒之后:`21:44:14Z` 到 `21:44:35Z` 之间,`curl -I …/-/<pkg>-0.21.30.tgz` 返回 `HTTP 200`,本地一次 `npm pack <pkg>@0.21.30 --registry https://registry.npmjs.org` 也成功写出了 tarball,而工作流 runner 自己的 `npm pack` 一直 `ETARGET` 到 `21:45:07Z` 把预算排空。同一个版本、两台机器、相隔一分钟给出相反答案。要回答"发布到底成没成",去 HEAD 那个 tarball URL 并试一次 pack——它们摸到的是产物本身——而且要从**正在失败的那台机器**上去试,因为"能取到"是一台机器才有的答案;`latest` 只回答"消费者默认拿到哪个版本",那是另一个问题,而且会更晚才稳定。`npm view <pkg> version` 在这之上还有第三条路径:它可能来自 npm 的**本地** packument 缓存,于是在发布前查过一次的机器上,即使全局 `latest` 已经前进,它仍会返回被取代的旧版本——这正是本门禁把目标版本固定取自 `PUBLISHED_VERSION`、并拒绝与 `package.json` 不一致的推断值的原因。不要因为一次元数据读取看起来过期就重推、移动或替换 tag;发布出去的 tag 是不可变的。
@@ -1,6 +1,6 @@
1
1
  # 路线图
2
2
 
3
- 0.21.52 已于 2026 年 10 月 24 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
3
+ 0.21.54 已于 2026 年 10 月 24 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
4
4
 
5
5
  ## 0.21.44 已完成范围
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-tab-worker-databus",
3
- "version": "0.21.53",
3
+ "version": "0.21.55",
4
4
  "description": "Framework-agnostic cross-tab data bus with Dedicated/Shared Worker clustering and Centrifuge support.",
5
5
  "type": "module",
6
6
  "license": "MIT",