Skip to content

fix(deps): take the fixes for proxy-addr, source-map-js and katex that turn main's OSV scan red - #21951

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21945-osv-lockfile-fixes
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21945-osv-lockfile-fixes

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #21945

Clause-②: no

What this does

Validate Package Dependencies runs OSV-Scanner against pnpm-lock.yaml. On main it reports four advisories, so the scheduled scan is red, and so is every PR that touches a package.json. Three of the four name a fixed version, and this PR takes those three fixes. The fourth, sprintf-js, has no fixed release. Its [[IgnoredVulns]] entry is on branch claude/issue-21945-osv-exemption, in its own osv-exemption PR, as convention 3 in osv-scanner.toml's header requires.

Which half this leaves: GHSA-hp3w-g68c-fv3c (sprintf-js). This PR alone does not turn the OSV step green, and neither does the exemption PR alone. The card stays open when this PR merges, and the PM finishes it after both have landed.

OSV reading: before and after

Measured locally with OSV-Scanner v2.3.8, the version validate-deps.yml pins. api.osv.dev answers 403 from this container, so the scan ran in offline mode (--offline-vulnerabilities --download-offline-databases) against the OSV npm database the scanner downloaded on 2026-10-06.

main at d16b9fbf (exit 1). These are the same four rows the scheduled run 37407261685 reported:

| https://osv.dev/GHSA-238p-pmpm-9mq7 | 2.1  | npm       | katex         | 0.16.47 | 0.18.2        | pnpm-lock.yaml |
| https://osv.dev/GHSA-jqcg-44mw-7w3h | 9.1  | npm       | proxy-addr    | 2.0.7   | 2.0.8         | pnpm-lock.yaml |
| https://osv.dev/GHSA-68fv-2mgg-jv7q | 8.7  | npm       | source-map-js | 1.2.1   | 1.2.2         | pnpm-lock.yaml |
| https://osv.dev/GHSA-hp3w-g68c-fv3c | 6.9  | npm       | sprintf-js    | 1.1.3   | --            | pnpm-lock.yaml |

This PR at e142f120 (exit 1, one row left, which the exemption PR covers):

| https://osv.dev/GHSA-hp3w-g68c-fv3c | 6.9  | npm       | sprintf-js | 1.1.3   | --            | pnpm-lock.yaml |

This PR's lockfile scanned with the exemption PR's osv-scanner.toml: exit 0, No issues found, with one vulnerability filtered.

Changes

pnpm-lock.yaml: proxy-addr 2.0.8 and source-map-js 1.2.2

Every parent's declared range already admits the fix: express 5.2.1 declares proxy-addr ^2.0.7, and postcss 8.5.28, @tailwindcss/node 4.3.3, css-tree 3.2.1 and magicast 0.5.3 declare source-map-js ^1.2.1. So this is a re-lock and needs no new pin.

  • Rejected: pnpm update proxy-addr source-map-js -r --depth Infinity. It re-resolved unrelated packages: @inquirer/*, @napi-rs/wasm-runtime, node-abi, a second postcss, nanoid and ip-address copy, and knex's peer set. That was +81/−60 lines.
  • Taken: a temporary override pair (proxy-addr to 2.0.8, source-map-js to 1.2.2), installed and then removed, followed by a second install. The lockfile keeps the two resolutions and nothing else moves: +11/−11 lines, the two package entries and the five dependent edges. pnpm-workspace.yaml ends that step byte-identical to main.

pnpm-workspace.yaml overrides: plus pnpm-lock.yaml: katex to ^0.18.2

  • Where: in pnpm-workspace.yaml. The root package.json has no pnpm.overrides (its pnpm field holds only ignoredBuiltDependencies), and the block's own header records that pnpm v10 reads overrides from this file.
  • Selector shape: 'katex@>=0.11.0 <0.19.0': '^0.18.2'. The floor is the advisory's introduced (0.11.0), and the bound sits at the caret boundary of the 0.18 target line. That is the durable shape the block header and check:override-consistency describe: the bound sits above the target's line, so a later lift moves only the target.
  • Resolution: ^0.18.2 resolves to 0.18.10, not 0.18.11, because npm deprecates 0.18.11 ("Accidentally published with breaking changes. Use 0.19.0 instead."). katex 0.18's CLI dependency moves commander 8.3.0 to 15.0.0, which was already in the tree, so commander@8.3.0 drops out. Lockfile +6/−11.
  • Why an override, and not a dedupe: no published mermaid admits the fix. Measured with npm view mermaid@V dependencies.katex: 11.16.0 and 11.16.1 declare ^0.16.45, and 11.17.0, 11.17.1, 11.17.2, 12.0.0 and 12.1.0 (latest) declare ^0.16.47. This override forces mermaid past its own declared range, so it needs evidence that mermaid still works.
  • Note: the entry carries a note in the block's style. It states the advisory, the forced-upgrade caveat and the evidence below.

Totals: pnpm-lock.yaml +17/−22; pnpm-workspace.yaml +33/−0 (one entry plus its note).

Evidence that katex 0.18 works for the only consumer

The only path to katex is apps/docs, then mermaid ^11.16.0 (11.16.1), then katex. Nothing else in the workspace names katex (git grep -i katex, lockfile excluded: zero hits).

  1. Where mermaid touches katex. It does so in one place, renderKatexUnsanitized in dist/chunks/mermaid.core/chunk-I66GZJ75.mjs. That is a lazy import("katex") followed by katex.renderToString(c, { throwOnError: true, displayMode: true, output }), where output is "mathml" or "htmlAndMathml". The katex 0.17 and 0.18 breaking changes (the internal __defineFunction API, and the prefixed internal CSS classes) touch neither that call nor the outer .katex class mermaid styles (.node .katex path). The 0.19.0 strict-mode change is outside the target line.
  2. Node. Through mermaid's own resolution, katex.version is 0.18.10, and renderToString with mermaid's options returns MathML for both output modes.
  3. Browser. A bundle of the real mermaid@11.16.1 mermaid.core.mjs ran in headless Chromium with the same initialize() options apps/docs/components/mermaid.tsx passes (securityLevel: 'strict'). It rendered a flowchart whose label is $$x^2 + \frac{a}{b}$$ with this result: {"katexVersion":"0.18.10","hasMath":true,"hasMsup":true,"hasFrac":true,"unsupported":false,"errored":false,"pageErrors":[]}.
  4. Docs build, local. next build in apps/docs ran under the shared verify lock (VERDICT command-exit 0; compiled in 119s; 1240/1240 static pages; BUILD_ID written). The client chunk carries katex version:"0.18.10", and no 0.16.47 remains in .next/static/chunks. This was next build alone, not the full vercel.json command: the docs app reads only packages/spec/package.json from the spec, and the committed content/docs/references stood in for gen:schema/gen:docs. The PR's Build Docs job runs the production command.
  5. Advisory direction. Under a polluted Object.prototype.trust, \href{…}{x} through katex 0.16.47 emits a link, and through 0.18.10 it does not.

No page in content/ puts $$ inside a mermaid block today, so this path is latent rather than live. The proof is still of the code that would run.

Changeset

skip-changeset. The diff touches pnpm-lock.yaml and pnpm-workspace.yaml, both repo-root configuration. Neither is in any package's files[], and overrides do not reach downstream installs, so nothing publishes.

Local verification, at e142f120

  • pnpm install --frozen-lockfile --prefer-offline: exit 0.
  • The gates come from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at this head: 22 commands, the same list the dispatch named. The --ran reconciliation is filled in below.
  • check:override-consistency: green. katex appears in neither census: mermaid consumes it, and the bound clears the target floor.

--ran reconciliation, with exit codes recorded before any pipe: 22 derived, 18 run, 4 NOT-MEASURED, 0 UNRUN.

Gate Exit
node scripts/check-changeset-fixed.mjs 0
node scripts/check-closing-keyword-parity.mjs (+ --self-test) 0 / 0
node scripts/check-comment-mask-corpus.mjs 0
node scripts/check-dts-emitted.mjs --self-test 0
node scripts/check-osv-exemptions.mjs (+ --self-test) 0 / 0
node scripts/check-prerelease-pin-watch.mjs --self-test ; --verbose 0 / 0
pnpm --filter @objectstack/spec run check:llms-txt 0
pnpm check:driver-memory-census · check:gitlink-declared · check:nul-bytes · check:override-consistency · check:refd-timer-probe · check:vendor-export-contract-resolve · check:watch-hint-literal · check:workspace-manifest-cycles 0 each
pnpm check:dts-closure · check:dual-build-cjs-loads · check:lean-entry-closure · check:sourcemap-no-sources-content 3, PREREQUISITE NOT MET

NOT MEASURED (four gates). They read every package's built dist/. This diff touches no package source, so the local scope builds no package, and the build these four need is the full pnpm build. CI's Build Core and Lint & Repo Gates measure them on the built tree. Both NOT MEASURED readings are declared here and are not counted as passes.

Not run locally, CI's: the path-scheduled jobs dispatch-gates lists (Test Core, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core, Build Docs) and the type-check lanes.

Acceptance notes

  • pnpm install prints ioredis-mock 8.13.1: unmet peer ioredis@^5: found 6.0.0. That warning is already present on main: the lockfile there resolves the same pair, and packages/services/service-cluster-redis/src/ioredis-pair.pin.test.ts pins it. It is not from this diff.

Generated by Claude Code

claude added 3 commits October 6, 2026 04:41
…jqcg-44mw-7w3h and GHSA-68fv-2mgg-jv7q

Both fixed versions are admitted by every parent's declared range
(express ^2.0.7; postcss, @tailwindcss/node, css-tree, magicast ^1.2.1),
so the lockfile only had to move off the two vulnerable resolutions.
Done through a temporary override pair, installed and then removed, so
no other resolution moved (11 insertions, 11 deletions); a plain
`pnpm update -r --depth Infinity` re-resolved eleven unrelated
packages and was rejected.

Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw
Co-authored-by: Claude <noreply@anthropic.com>
No published mermaid admits the fixed line (11.16.1 declares
^0.16.45; 11.17.0 through 12.1.0 declare ^0.16.47), so this is an
override past the dependent's declared range, not a dedupe. The
selector follows the overrides block's header rule: floor at the
advisory's 0.11.0, bound at the 0.19.0 caret boundary of the target
line. The target resolves to 0.18.10 because npm deprecates 0.18.11.
mermaid's only katex call (renderToString with throwOnError,
displayMode and output) renders MathML through 0.18.10 in Chromium.

Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw
Co-authored-by: Claude <noreply@anthropic.com>
…ut the katex entry

The note claimed the gate lists katex as an override it cannot
cross-check. Measured on this tree, it says nothing about the entry:
nothing publishable declares katex, mermaid consumes it, and the
selector's bound clears the target floor, so neither census reports it.

Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s dependencies Pull requests that update a dependency file labels Oct 6, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 6, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Correction to this PR's body, from the domain:devx seat 2 reviewer (session_01VF48aw8RPG6wzDnMgp6rtw), 2026-10-06T05:12Z. The dev's PR bodies are write-once, so the correction is recorded here.

The body says the local scan used "OSV-Scanner v2.3.8, the version validate-deps.yml pins". That is not what CI runs. validate-deps.yml:142 pins the action by sha with the comment # v2.3.8, but the job pulls the image ghcr.io/google/osv-scanner-action:v2.5.0. Scheduled run 37407261685, job 112087469784, step "Run google/osv-scanner-action/…" shows that pull.

Corrected reading: the local scans were measured with OSV-Scanner v2.3.8 and v2.5.0 (the image the CI job pulls). Both give identical rows on every tree. The verdicts in the body are unchanged.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 07:38
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 07:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 787104b Oct 6, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21945-osv-lockfile-fixes branch October 6, 2026 08:16
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…o fixed release exists) (objectstack-ai#21952)

Part of objectstack-ai#21945

Clause-②: no

## What this does

This PR adds one `[[IgnoredVulns]]` entry to `osv-scanner.toml`, for
**GHSA-hp3w-g68c-fv3c** (`sprintf-js` 1.1.3, DoS through unbounded
precision specifiers, CVSS 6.9). It is the only one of `main`'s four OSV
advisories with no fixed release. The ledger header's convention 3 says
an exemption lands in its own `osv-exemption`-labelled PR, so this PR
carries that entry and nothing else. The other three advisories are
fixed in objectstack-ai#21951 (lockfile re-lock plus a `katex` override).

**Which half this leaves:** the three fixable advisories, which are
objectstack-ai#21951's. This PR alone does not turn the OSV step green, and neither
does objectstack-ai#21951 alone. The card stays open when this PR merges, and the PM
finishes it after both have landed.

```toml
[[IgnoredVulns]]
id = "GHSA-hp3w-g68c-fv3c"
ignoreUntil = 2026-11-05
reason = "GHSA-hp3w-g68c-fv3c — no fixed sprintf-js exists (1.1.3, the latest release, is the last affected version), and it arrives only transitively through tedious 18.6.2 (driver-sql's optional mssql peer) and fengari 0.1.5 (under ioredis-mock, a service-cluster-redis devDependency), whose latest releases (tedious 20.3.3, fengari 0.1.5) still require ^1.1.3; remove when sprintf-js publishes a fix or both parents drop it."
```

The entry follows the header's conventions:

- **Convention 1:** `ignoreUntil` is a bare TOML date, 30 days out,
which is the default and well under the 90-day ceiling.
- **Convention 2:** `reason` is the advisory URL, an em dash, then one
sentence on why the advisory cannot be fixed right now.
- **One id only:** the scanner applies an exemption to the advisory's
aliases too, so the CVE alias (CVE-2026-97058) gets no entry of its own.

## Exemption decision

### There is no fix to take

| Fact | Reading |
|---|---|
| Advisory range (OSV offline npm DB, downloaded 2026-10-06) |
`introduced: 0` up to `last_affected: 1.1.3`, so there is no `fixed`
event |
| `npm view sprintf-js version` | `1.1.3` (published 2023-09-11, the
latest) |
| `tedious` (latest 20.3.3, `next` 20.3.4) | every major checked
(18.6.2, 19.0.0, 19.2.1, 20.0.0, 20.3.3, 20.3.4) declares `sprintf-js
^1.1.3` |
| `fengari` (latest 0.1.5) | declares `sprintf-js ^1.1.3` |
| `pnpm why -r sprintf-js` | one copy, 1.1.3; the only parents are
`fengari@0.1.5` and `tedious@18.6.2` |

No override can help, because there is no version to override to. Moving
either parent to a newer release changes nothing either.

### Where each parent is used in this workspace

- **`tedious` 18.6.2** is the MSSQL driver. `@objectstack/driver-sql`
declares it as an **optional peer**
(`packages/drivers/driver-sql/package.json:35`, with `optional: true` at
`:44`). The workspace installs a project's own peers (`.npmrc`
`auto-install-peers=true`), and `knex@3.3.0` picks it up as its optional
mssql peer, both under `driver-sql` and under `driver-sqlite-wasm`.
- In this repository it is reached only by driver-sql tests that build a
`client: 'mssql'` driver without a server:
`sql-driver-date-bucket.test.ts:163`,
`sql-driver-text-case-conformance.test.ts:366`,
`sql-driver-20446-empty-flip.test.ts:168` and
`sql-driver-20987-json-membership-move.test.ts:163`.
- The CLI's bundler keeps it external
(`packages/cli/src/utils/config.ts:95`).
- A downstream install that uses mssql brings its own `tedious`, and so
its own `sprintf-js`. This entry does not reach that install, and
nothing here can.
- **`fengari` 0.1.5** is a Lua VM in JavaScript. It sits under
`ioredis-mock` 8.13.1, directly and through `fengari-interop`, and
`ioredis-mock` is a **devDependency** of
`@objectstack/service-cluster-redis` (`package.json:33`).
- It runs only in that package's tests, where `ioredis-mock` executes
the package's two Lua scripts. Both are constants: `RELEASE_SCRIPT` at
`src/lock.ts:18` and `RENEW_SCRIPT` at `src/lock.ts:30`, sent by the
`eval` calls at `src/lock.ts:163` and `:186`.
- In production, `ioredis` sends `EVAL` to a real Redis, and `fengari`
is never loaded. The published package depends on `ioredis` only.

### Does either path pass an untrusted format string to `sprintf`?

The advisory's precondition is an attacker who controls the **format
string**, so that a precision specifier outside what `toFixed` /
`toExponential` / `toPrecision` accept throws a `RangeError`.

- **tedious: no.** All 12 `sprintf` calls in `tedious@18.6.2/lib` take a
string-literal format, and data only ever enters as an argument:
  - `value-parser.js:409` and `:485`;
  - `metadata-parser.js:104` and `:355`;
  - `prelogin-payload.js:204`;
  - `login7-payload.js:397` (four calls);
  - `packet.js:131`, `:144` and `:155`.
- **fengari: only from Lua source, and only partly.** The Lua script
supplies the format.
- `string.format` (`fengari/src/lstrlib.js:334`) passes the script's own
format to `sprintf` at `:361`, `:373` and `:391`.
- It does so only after `scanformat` (`:301`) caps width and precision
at two digits and raises `invalid format (width or precision too long)`
(`:314`). So the advisory's over-100 precision never reaches `sprintf`.
Measured: `string.format("%.100f", 1.5)` is a Lua error, and `"%.99f"`
formats normally.
- The lower edge, `"%.0g"` (`toPrecision(0)`), does still reach
`sprintf`. It fails the Lua call (measured: non-zero status).
- The other three `sprintf` calls in that file (`:172`, `:188` and
`:285`) use constant formats.
- Exposure therefore needs an attacker who can write the Lua source that
`ioredis-mock` runs. In this workspace the only Lua source is the two
constant scripts above, in tests.

**Net:** this repository passes no untrusted format string to `sprintf`
on any path.

### Routes considered and not taken

- **Drop `tedious` from `driver-sql`'s optional peers.** This removes a
published peer declaration, which is a contract change for MSSQL users.
The four tests above build an mssql client, and their own comments say
knex resolves `tedious` from this workspace, so they would need a
different stand-in client (not measured here). It would also not clear
`fengari`. That is outside this card.
- **Replace `ioredis-mock`.** That would mean a test-double migration
for `service-cluster-redis`, whose version pair is pinned in
`src/ioredis-pair.pin.test.ts`. It would not clear `tedious`. Also
outside this card.

Both parents would have to go for the advisory to leave the lockfile, so
neither route is a fix on its own.

### Renewal trigger

`ignoreUntil = 2026-11-05`. After that date the scanner stops filtering
the advisory and the step goes red on its own. **Remove the entry when
`sprintf-js` publishes a fix, or when both `tedious` and `fengari` drop
it.** Removing it can ride along with that fix, while a renewal is a new
decision and needs its own `osv-exemption` PR.

## OSV reading at this head (`e6a3636f`)

Measured locally with OSV-Scanner **v2.3.8**, the version
`validate-deps.yml` pins. `api.osv.dev` answers 403 from this container,
so the scan ran in offline mode against the OSV npm database the scanner
downloaded on 2026-10-06. Exit 1, with `sprintf-js` filtered and three
rows left, which objectstack-ai#21951 fixes:

```text
GHSA-hp3w-g68c-fv3c and 1 alias have been filtered out because: GHSA-hp3w-g68c-fv3c — no fixed sprintf-js exists …
Filtered 1 vulnerability from output
| https://osv.dev/GHSA-238p-pmpm-9mq7 | 2.1  | npm       | katex         | 0.16.47 | 0.18.2        | pnpm-lock.yaml |
| https://osv.dev/GHSA-jqcg-44mw-7w3h | 9.1  | npm       | proxy-addr    | 2.0.7   | 2.0.8         | pnpm-lock.yaml |
| https://osv.dev/GHSA-68fv-2mgg-jv7q | 8.7  | npm       | source-map-js | 1.2.1   | 1.2.2         | pnpm-lock.yaml |
```

This ledger scanned with objectstack-ai#21951's lockfile: exit 0, `No issues found`.

## Changeset

`skip-changeset`. `osv-scanner.toml` is repo-root configuration and
ships in no package's `files[]`.

## Local verification, at `e6a3636f`

The gates come from `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at this head: 13 commands. The
`--ran` reconciliation, with exit codes recorded before any pipe, is
**13 derived, 13 run, 0 NOT-MEASURED, 0 UNRUN**:

- `node scripts/check-changeset-fixed.mjs`: 0.
- `node scripts/check-closing-keyword-parity.mjs` (+ `--self-test`): 0 /
0.
- `node scripts/check-comment-mask-corpus.mjs`: 0.
- `node scripts/check-osv-exemptions.mjs` (+ `--self-test`): 0 / 0. The
verdict line reads `✓ 1 OSV exemption(s) in osv-scanner.toml: all carry
an unexpired ignoreUntil within 90 days and a reason with an advisory
link.`
- `pnpm check:driver-memory-census` · `check:gitlink-declared` ·
`check:nul-bytes` · `check:override-consistency` ·
`check:refd-timer-probe` · `check:vendor-export-contract-resolve` ·
`check:watch-hint-literal`: 0 each.

## Acceptance notes

- The ledger's header still says "This ledger currently holds ZERO
exemptions." Once this entry lands, that sentence is no longer true.
This PR leaves the header byte-identical so that its diff is the entry
alone. If the reviewer wants it, the one-line count edit can go into
this same PR.
- The `osv-exemption` label did not exist in this repository before this
PR: `GET /labels/osv-exemption` answered 404. No exemption has landed
since the convention was written. The additive label write on this PR
creates it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… decision in words instead of a tracker number (stage 25) (objectstack-ai#21975)

Part of objectstack-ai#20749
Clause-②: no

Stage 25 of this card: the next area of class (e), the test strings
shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513.
This stage takes the second and last name-ordered `api/` group: the 13
id-bearing test files directly under `packages/spec/src/api/` from
`plugin-rest-api.handler-status-retirement.test.ts` to
`zod-issues-to-fields.test.ts`. Those files carried 89 messages and 95
tracker ids, citing 43 records. All 95 now either state what their
record decided, in words (form D), or are dropped where the title
already says it. No needle sits in this group. Text only: no assertion,
identifier, test count or code comment changes, and no file is renamed.
With this stage, `api/` carries no tracker id in a test string.

## Census at the base (`5a22eb5619`)

Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`),
`census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs`
(md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5
`dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages
10 to 24 used. A literal counts as a test title when its folded message
is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` /
`.only` chains included. Everything else is an "other" string.

The worktree was cut from `origin/main` at `5a22eb5619`, the claim's
base and stage 24's landing. Both instruments read **371 messages / 392
ids in 84 files**, the seat's reading and stage 24's head reading.

| directory | files | messages / ids | titles | other |
|:--|--:|--:|--:|--:|
| `system/` | 34 | 154 / 167 | 128 / 138 | 26 / 29 |
| (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 |
| `api/` (this PR: all 13 files) | 13 | 89 / 95 | 86 / 92 | 3 / 3 |
| `ui/` | 5 | 7 / 7 | 0 | 7 / 7 |
| `ai/` | 1 | 2 / 2 | 0 | 2 / 2 |
| `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 |
| **total** | **84** | **371 / 392** | **331 / 349** | **40 / 43** |

The group reads **89 messages / 95 ids in 13 files**, the seat's figures
file for file:

| file (under `api/`) | messages / ids | titles | other |
|:--|--:|--:|--:|
| `plugin-rest-api.handler-status-retirement.test.ts` | 4 / 4 | 3 / 3 |
1 / 1 |
| `plugin-rest-api.schema-refs.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `plugin-rest-api.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `protocol.test.ts` | 46 / 50 | 46 / 50 | 0 |
| `registry-retirement.test.ts` | 2 / 2 | 1 / 1 | 1 / 1 |
| `rest-api-config-dead-keys-retirement.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `rest-server.test.ts` | 19 / 19 | 18 / 18 | 1 / 1 |
| `router.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `sortability.test.ts` | 3 / 4 | 3 / 4 | 0 |
| `storage.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `validate-data.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `websocket.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `zod-issues-to-fields.test.ts` | 2 / 3 | 2 / 3 | 0 |
| **13 files** | **89 / 95** | **86 / 92** | **3 / 3** |

Five more test files sit in the same name range and carry no id
(`query-adapter.test.ts`, `realtime-shared.test.ts`, `realtime.test.ts`,
`retired-error-codes.test.ts`, `versioning.test.ts`). The three "other"
strings are expect failure messages, rewritten and declared to the
text-only tool: `plugin-rest-api.handler-status-retirement.test.ts:179`
and `rest-server.test.ts:768` (template literals) and
`registry-retirement.test.ts:89` (one leaf of a `+` chain).

- **Controls.** Lit: `ui/notification.test.ts` (1 id) and
`system/book.test.ts` (2 ids), outside the group, read the same at the
base and at the head. Dark: `protocol.test.ts` reads 0 at the head while
65 of its lines still carry a number, every one of them a comment.
Planted in a scratch tree: an id put into a `storage.test.ts` title
reads 1 / 1 (`title:it`), and an id put into a `sortability.test.ts`
comment reads 0.
- **A wider pattern** (any `#` plus digits) reads the same as the gate
pattern in all 13 files at the base, and 0 in all 13 at the head.
- **At the head:** 282 messages / 297 ids in 71 files. The 13 files read
0 / 0, `api/` leaves the table, and no other file moved.

## How the area was chosen

`api/` has no subdirectory, so it is taken in name-ordered file groups
near the ~100-id bound, the rule stages 20 to 24 used. Stage 24's cut
named this group at 95 ids, and this census reads 95, so no re-cut was
needed. `protocol.test.ts` (50 ids) fits one PR and one text-only proof,
so it is not split.

**Named for the next stages** (cut from the head census, 282 / 297):
- **`system/`**, 167 ids in 34 files (one of them in
`system/constants/`), two stages:
- **first group:** `auth-config.test.ts` through
`metadata-form-declared-rows.pin.test.ts`, 18 files, 91 messages / 97
ids (`i18n-resolver.test.ts` alone 53 / 56);
- **second group:** `metadata-form-zod-reconciliation.test.ts` through
`worker.test.ts`, 16 files, 63 / 70. Its first file carries 17 "other"
strings, its ledger `why` entries.
- The files directly in `src/`, 120, one stage.
- The needles: the three docblock needles, the kept
`ui/component-props-unknown-members.pin.test.ts:322` and stage 22's two.
One stage, with an at-tier review. The four colour literals stay, as
stage 21 decided.

## What each id became

- **10 literals (11 ids)** now state a decision in words.
- **12 literals (16 ids)** get their subject back in words, where the
number stood for a thing.
- **67 literals (68 ids)** drop a number the title already explains.

Every cited record was fetched with all its comments through REST, and
its decision was read from its ruling, ACCEPT and landing comments: a
keyword digest of every record, and full reads wherever the new words
carry a decision. 43 records are cited: 36 answer 200 and 7 answer 404.
Two more were read for context: objectstack-ai#14478, whose ruling B objectstack-ai#15677 executes,
and PR objectstack-ai#11426, objectstack-ai#11006's landing. The seven that answer 404 were read
from what landed, through the commits endpoint (this checkout is
shallow), each commit found through the CHANGELOG entry or the commit
list of `protocol.test.ts`:
- **objectstack-ai#6037**, from `18189983dd` (objectstack-ai#6474): `DataProtocol.validateData` asks
the write path for its verdict and persists nothing, objectstack-ai#4633 ruling D;
- **objectstack-ai#6239**, from `f549a0d4ad` (objectstack-ai#6526): `ViewProtocol`'s five
viewId-addressed methods and ten schemas are retired;
- **objectstack-ai#6361**, from `90bbf25107` (objectstack-ai#6866): the notification-list `cursor`
is tombstoned on both halves (maintainer ruling 2026-08-07, option A);
- **objectstack-ai#9740**, from `11b779e0f9` (objectstack-ai#9773):
`MetadataProtocol.getMetaItemLayered` is declared, and the dead
`'overlay'` `lockSource` arm is dropped;
- **objectstack-ai#9741**, from `2a29caa532` (objectstack-ai#9804): `previewDrafts` / `state` are
declared where the implementation enforces them, and `environmentId` is
recorded as transport-level. Its changeset
(`packages/spec/CHANGELOG.md:31798`) names it "maintainer ruling
2026-08-18", and `cccbe51bf7` cites "the objectstack-ai#9741 ruling";
- **objectstack-ai#11006**, from `cccbe51bf7` (objectstack-ai#11426): `publishMetaItem` is declared
as an optional member with `PublishMetaItemRequest` (maintainer ruling
2026-08-22, option B);
- **objectstack-ai#14691**, from `b3a63d32c9` (objectstack-ai#14868): the ten inert
`RestServerConfig` keys the liveness ledger recorded as `dead` are
retired.

**The same-id titles stage 24 listed in this group:**
- **`[objectstack-ai#5672]` x2** (`protocol.test.ts:508`, `:526`): objectstack-ai#5672's maintainer
ruling A (`5199159328`): one closed capability vocabulary, emitted in
full by both discovery producers, with an absent capability `enabled:
false` rather than a missing key. `:508` now reads "strips a capability
key outside the closed vocabulary". The old verb was "rejects", but the
body pins the opposite: the parse stays green and the key does not
survive it. `:526` now reads "… (ruled: an absent capability is
`enabled: false`, not a missing key)".
- **`(objectstack-ai#12038)` x5** (`:2575` to `:2686`): these five "declares the …
body" describes are the describe-only transcriptions that the five-part
ruling's implementation plan names (`5434804846`). None of them pins a
lettered sub-ruling, so no letter is named; the title already says the
decision, and only the number goes.
- **`(objectstack-ai#12038 1C)`** (`:2710`): now "GetPublishedMetaItemResponseSchema
stays opaque (ruled: no shape frozen against the current type
registry)", ruling 1C's own reason. Its children pin the `unknown` body.
- **`(objectstack-ai#19543, door ③)`** (`:2726`): door ③ is the AI-conversation list,
which the schema name already names, and "declares the next-page signal"
is that door's spec half (letter A, re-derivation `5825819437`). Only
the number and the door label go.
- **`(objectstack-ai#15677)`** in `plugin-rest-api.test.ts:694` and
`websocket.test.ts:712`: now "… durations carry their unit in the key
name", objectstack-ai#14478's ruling B (`5518649320`, population ruling `5548763981`),
which objectstack-ai#15677 executes for `api/`. In `router.test.ts:565` the title
already shows the rename (`RouteDefinition.timeout → timeoutMs`), so
only the number goes.

**Stated in words:**

| record | literal (under `api/`) | now reads | the decision |
|:--|:--|:--|:--|
| objectstack-ai#14478 via objectstack-ai#15677 | `plugin-rest-api.test.ts:694`,
`websocket.test.ts:712` | "… durations carry their unit in the key name"
| Ruling B: a `z.number()` duration key carries its unit in its name;
the old spellings are `retiredKey()` tombstones. |
| objectstack-ai#5672 | `protocol.test.ts:508` | "strips a capability key outside the
closed vocabulary" | Ruling A (2026-08-06): one closed vocabulary. |
| objectstack-ai#5672 | `protocol.test.ts:526` | "rejects a capability map that is
missing part of the vocabulary (ruled: an absent capability is `enabled:
false`, not a missing key)" | Ruling A: both producers emit the whole
vocabulary. |
| objectstack-ai#9406 | `protocol.test.ts:1313` | "probes is opaque BY DECLARATION
(ruled: modeled only once a consumer needs a field): …" | Maintainer
ruling 2026-08-18 (`5322875103`): `probes` gets a deliberately opaque
passthrough, upgraded to a modeled schema only when a consumer needs a
field of it. |
| objectstack-ai#9343 | `protocol.test.ts:1383` | "PublishPackageDraftsResponseSchema
published[].advisories (ruled: advisory findings ride each published
element)" | Maintainer ruling 2026-08-17 (`5321046016`): `advisories`
rides each `published[]` element, with no parallel top-level map. |
| objectstack-ai#9741 | `protocol.test.ts:1739` | "environmentId stays OUT of the
meta-read request shape — transport-level by decision, not omission" |
The 2026-08-18 ruling, as landed in `2a29caa532`: `environmentId` is the
transport-level multi-kernel routing key. |
| objectstack-ai#12038 | `protocol.test.ts:2710` | "GetPublishedMetaItemResponseSchema
stays opaque (ruled: no shape frozen against the current type registry)"
| Ruling 1C (`5434804846`): a thin envelope with the body opaque, no
union frozen against today's type registry. |
| objectstack-ai#6037 | `validate-data.test.ts:25` | "ValidateDataRequest — asks the
write path for its verdict instead of predicting it" | What landed in
`18189983dd`: the dry run stops predicting the write's verdict and asks
for it. |
| objectstack-ai#6037 | `validate-data.test.ts:57` | "ValidateDataResponse — the
verdict the write path would reach, persisting nothing" | The same
commit: `validateData` reports the write path's verdict on candidate
rows and persists nothing. |

**Subject back in words** (12 literals):
- "zero holders after objectstack-ai#13823" becomes "zero holders after its
retirement", and "[objectstack-ai#13823] ADR-0087 registration" becomes "handlerStatus
retirement — ADR-0087 registration", the form of the repo's other
retirement registration describes (objectstack-ai#13823 ruled remove, `5494755488`);
- "the routes wired in objectstack-ai#3899" becomes "the routes wired to the
request-schema gate", the gate the file's header names;
- "(objectstack-ai#13155 — carries objectstack-ai#5745 to the third verb)" becomes "(carries the
declared = returned discipline to the third verb)", the discipline objectstack-ai#7294
and objectstack-ai#13155 name objectstack-ai#5745 for;
- "(objectstack-ai#4717 — objectstack-ai#4463 D3 on the response)" and "(objectstack-ai#9176 — objectstack-ai#4463 D3 on the
publish door)" become "(advisory findings ride the 2xx response)" and
"(advisory findings ride the 2xx on the publish door too)": objectstack-ai#4463's D3
sends gating findings to 422 and lets advisory findings ride the 2xx;
- "the objectstack-ai#9612-gate class" becomes "the package-closure publish-gate
class": objectstack-ai#9612's gate judges a publish against the written package's
closure;
- "objectstack-ai#10235 the objectstack-ai#7865 anchor category" becomes "the unprovisioned
injected-anchor category", the platform anchors injected into an
external object whose storage the platform does not provision (objectstack-ai#7865,
ruling B);
- the two "pre-objectstack-ai#3689" storage shapes become shapes "from before the
shared success envelope";
- "the objectstack-ai#4052 non-repeat" becomes "the non-repeat of the retired
`validateOnly` dry-run flag";
- "every objectstack-ai#8055-shaped fixture" becomes "every malformed-flow-body
fixture".

**Dropped where already stated** (67 literals, 68 ids). A number goes
only where the title already says its decision. Examples: the two
`[objectstack-ai#13823]` describes and the twelve `[objectstack-ai#14691]` / `(objectstack-ai#14691)` retirement
titles ("REJECTS `patterns` with the retirement prescription — …", "the
tombstones reject one key each, not the config — …"); `[objectstack-ai#11983]` x3,
`[objectstack-ai#4579]` x2, `[objectstack-ai#4939]`, `[objectstack-ai#6361]`, `[objectstack-ai#20294]` and `objectstack-ai#3899 —`; the
`objectstack-ai#10235` prefixes on "resolveObjectSortability — the closed category
set" and "wire validity — …"; the five "transport-level by the objectstack-ai#9741
ruling" titles, which now read "transport-level by ruling"; the
parenthesized `(objectstack-ai#5745 — …)`, `(objectstack-ai#7294 — …)`, `(objectstack-ai#9406 — …)`, `(objectstack-ai#10524 —
…)` x2, `(objectstack-ai#9726 — …)`, `(objectstack-ai#9741 — …)` and `(objectstack-ai#4717 — …)` pairs, which keep
their words; and the tails `(objectstack-ai#6239)`, `(objectstack-ai#4286)`, `(objectstack-ai#9740)`, `(objectstack-ai#11006)`
x2, `(objectstack-ai#11678)` x3, `(objectstack-ai#9426)`, `(objectstack-ai#12005)` x3, `(objectstack-ai#11679)` x2, `(objectstack-ai#12004)`
x2, `(objectstack-ai#3718)`, `(objectstack-ai#4572)`, `(objectstack-ai#4579)`, `(objectstack-ai#20294)`, `(objectstack-ai#8124/objectstack-ai#8055)`, the
five `(objectstack-ai#12038)` and the `(objectstack-ai#4738, …)` aside in one expect message. The
404 numbers among them (objectstack-ai#6239, objectstack-ai#6361, objectstack-ai#9740, objectstack-ai#9741, objectstack-ai#11006, objectstack-ai#14691) go
only where the title already states what landed.

**No file is renamed.**

## Readers

- **Needles:** none. The three declared strings are assertion failure
messages (the second argument of `expect`), none is an expected value,
and no title or message in the group is matched against a source
docblock or another file's text. The one self-read in the group,
`rest-api-config-dead-keys-retirement.test.ts:519`, reads its own file
for the id-free describe title "tree-scoped absence", which this PR does
not touch.
- **Test-name filters:** none. No tracked script, workflow or package
config passes `-t` / `--testNamePattern` to vitest; the one vitest `-t`
hit is a README example under `packages/qa/dogfood` filtering its own
fixture.
- **Snapshots:** none. No `__snapshots__` directory is tracked under
`packages/spec`, and none of the 13 files calls a snapshot matcher.
- **Projects:** `rest-api-config-dead-keys-retirement.test.ts` is in the
`repo` project (`packages/spec/vitest.repo-tests.json:31`); the other 12
run in `local`. The base-versus-head run below takes both projects.
- **By substring:** every old literal, its id-bearing fragment and a
window around each id (270 needles) was searched with `git grep` at the
base, across the tracked tree outside its own file. No gate, doc,
filter, snapshot, QA checklist entry or `scripts/check-*.mjs` self-test
reads one. The 6 hits are sibling test titles: the two `(objectstack-ai#15677)`
describes in this group hit each other (both rewritten here),
`client/src/client.test.ts:1134` shares "query.distinct (objectstack-ai#4286)", and
`metadata-protocol/src/protocol.validate-data.test.ts:102` shares "the
objectstack-ai#4052 non-repeat".

## Text-only proof

Stage 10's scratch tool (`textonly10.cjs`, md5
`d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file
on three legs:
1. **Skeleton:** the full AST, with string pieces masked. It must be
identical.
2. **Comments:** every comment, byte-equal.
3. **Strings:** each changed string leaf must sit in a test-call title
position or on a declared line, must carry a tracker id before, and must
carry no `#` plus digits after. This stage declares the three
expect-message lines named above.

- **Result:** 13 of 13 files SAME on all three legs, with the per-file
counts predicted in writing before any edit.
- **Totals:** 89 changed string leaves in 89 literals: 86 titles and 3
declared. The diff's `+` and `-` lines are exactly the 89 planned lines
as multisets, and every file keeps its line count.
- **Controls (14 of 14 as predicted on the first run, on scratch copies,
each anchor hit once):** identifier rename DIFF; numeric literal DIFF;
comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a
rewritten title given a new id VIOLATION; a title that was id-free at
base edited VIOLATION; one title reverted to base SAME; an `it.each` row
given an id VIOLATION; an undeclared expect message changed VIOLATION; a
title re-split into a `+` chain DIFF; a declared expect message reverted
to base SAME; a declared template expect message given a new id
VIOLATION; a declared `+`-chain leaf given a new id VIOLATION; a
template-literal title given a new id VIOLATION.
- **Templates and tables:** no `.each` title and no `$name` placeholder
changes. The two template literals change only their text after the
`${…}` span.

**Test counts:** the 13 files were run at the base, in a separate base
worktree, and at the head, with `--project local --project repo`. Both
sides read 509 tests in 13 files, all passed, with the same count and
status sequence per file in 13 of 13. 250 full test names change, and
each changed name equals the base name with the planned replacements
applied: 0 mismatches. No full name repeats on either side, and no head
name carries `#` plus digits (250 base names did). `router.test.ts:565`
writes its arrow as a `→` escape; the plan's anchor there starts after
the escape, so the comparison tool, which reads escapes literally, met
none, and vitest prints "RouteDefinition.timeout → timeoutMs …" on both
sides.

## Changeset: `skip-changeset`

Measured, not assumed:
- `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the
13 touched files are in it, and no `*.test.ts` at all. The controls
`src/api/protocol.zod.ts`, `src/api/rest-server.zod.ts` and
`dist/index.mjs` are in it.
- In the built `dist/`, two new phrases and an old one each read in 0
files. The control `Unrecognized key` reads in 42.

So this PR publishes nothing, and no changeset is added.

## Verification (at `c63eba0adf`)

- `pnpm turbo run build` over all packages: 71 / 71, through the shared
verify lock (`VERDICT command-exit 0`).
- `@objectstack/spec`:
  - `vitest run --project local`: 619 files, 18480 passed, 1 todo.
- `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246
errors / 135 pinned signatures held). Its program holds all 13 group
files, counted by path with `tsc --listFilesOnly -p tsconfig.test.json`.
- `check:generated`: all 15 generated artifacts up to date, against the
`dist/` the build above wrote.
- **Gates:** `dispatch-gates --commands` derived 79 families: stage 24's
80 without `check:error-code-casing`, whose named sources this diff does
not touch. All 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0
NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. The
same 79 derive from `origin/main` `230e4944b0` with this diff applied.
The five roster families marked as sharing a directory with this diff
(`check:meta-url-spelling`, `check:spec-changes`,
`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`) each exit 0.
- **ESLint, a proven narrowing:** `--no-inline-config` over the 13 files
reads 0 errors and 0 warnings. The population comes from ESLint's own
config: 13 configured, 0 ignored. No file sets `parserOptions.project`
or `projectService`, so no untouched file's verdict can move.
- `check-governed-merges --test`: NOT governed, 178 changed lines (+89 /
-89).
- A control-byte scan over the 13 changed files finds none.

## `main` since the base

Re-fetched just before this PR opened, `origin/main` was six commits
past the base (`c9761cd2fb`: objectstack-ai#21966, objectstack-ai#21951, objectstack-ai#21963, objectstack-ai#21969, objectstack-ai#21965,
objectstack-ai#21962). They touch 28 files, none of the 13 and none under
`packages/spec/src/api/`, so `main` was not merged. The two
`packages/spec/src` files they change
(`data/datasource-credential-redaction.ts` and its test) read 0 / 0 in
the census at `c9761cd2fb`: the one id they add is a code comment. `git
merge-tree` onto `c9761cd2fb` is clean, and none of the 4 open PRs
touches any of the 13 files.

## Acceptance notes

- **Same-id test titles in this card's later stages** go with those
stages: `system/book.test.ts:413` (`(objectstack-ai#12038)`).
- **Same-id test titles in other packages** stay: 97 lines in 15
packages (`runtime` 25, `objectql` 13, `lint` 12, `metadata-protocol`
12, `rest` 12, `client` 8, `metadata-core` 4, `qa/dogfood` 2,
`service-automation` 2, `service-storage` 2, and one each in
`examples/app-crm`, `examples/app-showcase`, `driver-sql`,
`plugin-sharing` and `types`), each package's share under the objectstack-ai#20513
lane children.
- **Code comments with live ids** remain in these files and their
sources, among them the `* objectstack-ai#3899 —` header in
`plugin-rest-api.schema-refs.test.ts`, the `* objectstack-ai#8124 —` header in
`zod-issues-to-fields.test.ts`, the `// [objectstack-ai#5672] This fixture used to
lead with …` comment above `protocol.test.ts:508`, and the `/** [objectstack-ai#20294]
… */` docblock in `rest-api-config-dead-keys-retirement.test.ts`. Code
comments are not this card's share.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…OSV scan red (objectstack-ai#22016)

Fixes objectstack-ai#22013

Clause-②: no

## What this does

`Validate Package Dependencies` runs OSV-Scanner against
`pnpm-lock.yaml`. Two advisories published on 2026-10-06 match `main`'s
lockfile, so every PR that touches a `package.json` inherits a red that
is not its own (PR objectstack-ai#22002, run 37483796939, is the first measured), and
the next scheduled scan will be red too. Both advisories name a fixed
version, so this PR takes both fixes. There is no exemption:
`osv-scanner.toml` is untouched, because it is for advisories with no
fixed release.

PR objectstack-ai#22002 is not touched here. Its `update-branch` after this lands is
the PM's pointer to its holder.

## OSV reading: before and after

Measured locally with OSV-Scanner **v2.3.8**, the version
`validate-deps.yml` pins, in offline mode (`--offline-vulnerabilities
--download-offline-databases`). The OSV npm database was downloaded on
2026-10-06 at 16:36 UTC, after both advisories were published (13:40 and
13:43 UTC).

`main` at `803764a3` (exit 1). These are the two rows the card names:

```text
| https://osv.dev/GHSA-wq5f-xc86-pv6w | 8.9  | npm       | sharp       | 0.35.4  | 0.35.5        | pnpm-lock.yaml |
| https://osv.dev/GHSA-pqg4-j6r4-53mv | 9.2  | npm       | shell-quote | 1.10.0  | 1.11.0        | pnpm-lock.yaml |
```

This PR at `d263b701` (exit 0): `No issues found`. One vulnerability is
filtered, the standing `sprintf-js` exemption, unchanged. The scanner
names nothing beyond these two advisories, so no third one has appeared
since the card was filed.

## Changes

### `sharp`: the override target lifts from `^0.35.4` to `^0.35.5`

- **Advisory:** GHSA-wq5f-xc86-pv6w (8.9 high). It is a memory bug in
the librsvg that sharp's prebuilt binaries bundle. The range is
introduced 0, fixed 0.35.5, read from the scanner's offline database.
- **Edit:** the target only, `'sharp@>=0.34.0 <0.36.0': '^0.35.5'`. The
selector already sits at the 0.x caret boundary, so it does not change.
This is the same shape as the objectstack-ai#16999 lift on this entry.
- **Dedupe, not a forced upgrade:** `next@16.3.6` declares the optional
`sharp: ^0.35.4`, which already admits 0.35.5. 0.35.4 was the single
resolved copy.
- **What moves with it:** sharp pins its prebuilt `@img/sharp-*`
binaries exactly, so they move to 0.35.5, and the libvips binaries move
to 1.3.4.
- **Measured:** the installed sharp 0.35.5 loads on linux-x64. It
reports `rsvg 2.63.2`, the librsvg release the advisory names as fixed,
and it renders a PNG.

### `shell-quote`: a new override, `'shell-quote@>=1.8.4 <2.0.0':
'^1.11.0'`

- **Advisory:** GHSA-pqg4-j6r4-53mv (9.2 critical). In `quote()`, a line
terminator in a string after a `{ comment }` token ends the comment, and
the rest of that string runs as shell input. The range is introduced
1.8.4, fixed 1.11.0.
- **The one path:** `launch-editor@2.14.1`, then
`@changesets/cli@3.0.3`, then the root `package.json`'s
`devDependencies`. launch-editor declares `shell-quote: ^1.8.4`, which
admits the fix, so this is a dedupe onto the patched line. The copy sat
on 1.10.0 through lockfile inertia.
- **Selector shape:** the floor is the advisory's 1.8.4, and the bound
sits at the 2.0.0 major boundary, per the block header's rule. The bound
is never `<1.11.0`.
- **Resolution:** `^1.11.0` floats to **1.12.0**, the newest 1.x.
launch-editor calls shell-quote in one place, `parse()` on the editor
command (`guess.js`). 1.11.0 and 1.12.0 only teach `parse()` ANSI-C
quoting and more operators. Seven editor command strings (`code --wait`,
a quoted macOS path with `-w`, `emacsclient -t -a ''` and others) parse
identically under 1.10.0 and 1.12.0. The vulnerable `quote()` is never
called on this path. The fix is taken anyway, because the gate reads the
lockfile, not the call graph.
- **Note:** the entry carries a note in the block's style, at the foot
of `overrides:`.

### Why an override and not a `@changesets/cli` bump

No release on that path forces the fix (`npm view`, 2026-10-06):

- `@changesets/cli` 3.0.3 is npm `latest`, which is the version the root
already declares. Every 3.0.x declares `launch-editor: ^2.14.1`.
- launch-editor's latest, 2.14.2 (published today), declares
`shell-quote: ^1.10.0`, which still admits the flagged 1.10.0.

So the bump route does not exist, and a launch-editor bump would still
leave the floor to lockfile inertia. On the four axes:

- **Real need:** the measured need is a patched resolution and a green
gate. Only the override delivers both, because no upstream release
declares a range above 1.10.0.
- **Long-term soundness:** the entry follows the block header's selector
rule (bound at the major boundary), so a later advisory is a target-only
lift. It turns into a dedupe floor once launch-editor declares
`^1.11.0`. Its cost is one more ledgered entry.
- **Making AI mistakes harder:** a declared floor is audited by
`check:override-consistency`, and no re-lock can land below it. A bare
re-lock with no floor, like objectstack-ai#21951's `proxy-addr` step, leaves nothing
to stop a later resolution from drifting back.
- **Startup scope:** this is the smallest change. It moves no
devDependency and adds no gate.

## Lockfile diff

`pnpm-lock.yaml` is **+126/−125**, re-locked by `pnpm install`, never by
hand. Every changed line falls into one of four kinds:

- a `sharp`, `@img/sharp-*` or `shell-quote` package or snapshot key;
- a dependency edge onto one of those packages;
- the integrity line under one of those keys;
- one of the two `overrides:` header lines, which are the sharp target
and the new shell-quote entry.

Nothing else moves. This was measured by attributing every changed line:
after dropping the lines that name sharp or shell-quote, and the
integrity lines under those keys, zero lines remain.

`pnpm-workspace.yaml` is **+39/−1**: the sharp target, its dated note,
and the shell-quote entry with its note.

`pnpm why`, before and after:

- `sharp` 0.35.4 becomes 0.35.5, one version in both cases. The tree
shape is byte-identical with the version masked: through `next@16.3.6`
under `@objectstack/docs` (and the fumadocs packages), and through
better-auth's `next` peer under `@objectstack/plugin-auth`.
- `shell-quote` 1.10.0 becomes 1.12.0, one version in both cases:
`launch-editor@2.14.1`, then `@changesets/cli@3.0.3`, then the root's
devDependencies.

## Changeset

`skip-changeset`. The diff touches `pnpm-lock.yaml` and
`pnpm-workspace.yaml`, both repo-root configuration, and neither is in
any package's `files[]`. sharp resolves under two importers:

- `@objectstack/docs`, which is private.
- `@objectstack/plugin-auth`, which is published. Its `files[]` is
`dist`, `README.md` and `CHANGELOG.md`, and its `package.json` does not
change. It declares no sharp range at all. sharp reaches it only through
better-auth 1.7.3's optional `next` peer, which this workspace
auto-installs (`auto-install-peers=true`), and next's own optional
`sharp` dependency.

Overrides do not reach downstream installs, so nothing published
changes. shell-quote is dev-only, under the private root.

## Local verification, at `d263b701`

- `pnpm install --frozen-lockfile --prefer-offline`: exit 0.
- The gates come from `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`, derived from the change set at this
head: 22 commands. The dispatch named 25 because it included
`package.json`, which this diff does not touch. The `--ran`
reconciliation is filled in below.

`--ran` reconciliation, with exit codes recorded before any pipe: **22
derived, 18 run, 4 NOT-MEASURED, 0 UNRUN**.

| Gate | Exit |
|---|---|
| `node scripts/check-changeset-fixed.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs` (+ `--self-test`) | 0
/ 0 |
| `node scripts/check-comment-mask-corpus.mjs` | 0 |
| `node scripts/check-dts-emitted.mjs --self-test` | 0 |
| `node scripts/check-osv-exemptions.mjs` (+ `--self-test`) | 0 / 0 |
| `node scripts/check-prerelease-pin-watch.mjs --self-test` ;
`--verbose` | 0 / 0 |
| `pnpm --filter @objectstack/spec run check:llms-txt` | 0 |
| `pnpm check:driver-memory-census` · `check:gitlink-declared` ·
`check:nul-bytes` · `check:override-consistency` ·
`check:refd-timer-probe` · `check:vendor-export-contract-resolve` ·
`check:watch-hint-literal` · `check:workspace-manifest-cycles` | 0 each
|
| `pnpm check:dts-closure` · `check:dual-build-cjs-loads` ·
`check:lean-entry-closure` · `check:sourcemap-no-sources-content` | **3,
PREREQUISITE NOT MET** |

- **`check:override-consistency`:** the census now counts 38 overrides,
up from 37. shell-quote appears in neither report: it has a consumer,
and its bound clears the target floor.
- **NOT MEASURED (four gates):** they read every package's built
`dist/`. This diff touches no package source, so the local scope builds
no package, and the build these four need is the full `pnpm build`. CI's
`Build Core` and `Lint & Repo Gates` measure them on the built tree.
This narrowing is declared here, and none of the four is counted as a
pass.
- **Not run locally, CI's:** the path-scheduled jobs that
`dispatch-gates` lists (`Test Core`, `Temporal Conformance`, `Dogfood
Regression Gate`, `Dogfood Verify CLI`, `Build Core`, `Build Docs`) and
the type-check lanes.

## Acceptance notes

- `pnpm install` prints `ioredis-mock 8.13.1: unmet peer ioredis@^5:
found 6.0.0`. That warning is already on `main` and is not from this
diff.
- The sharp selector's floor is 0.34.0, while the new advisory's range
starts at 0. No sharp copy below 0.34 resolves anywhere, and the card
rules the selector untouched, so the floor stays where it is.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…rge base; main keeps the absolute daily scan; job timeout 15 min (objectstack-ai#22138)

Fixes objectstack-ai#22082

Clause-②: no

## What this does

Implements ruling A as recorded on the card (comment 6049727506). On
`pull_request`, the OSV verdict of `validate-deps.yml` is now
base-relative. On `schedule` and `workflow_dispatch`, `main` keeps the
absolute verdict from the same step as before. That step's only change
is `if: github.event_name != 'pull_request'` (its version comment is
also corrected, see below). The job gains `timeout-minutes: 15`, sized
from the measured p50 of 1.3 min and max of 1.9 min.

- **Lockfile unchanged.** The new stage step compares the pull request's
test merge with its merge base. If neither `pnpm-lock.yaml` nor
`osv-scanner.toml` differs, the step prints the notice "lockfile
unchanged — inherits main's verdict" and runs no scan. The ledger counts
as part of the input because a PR that deletes an exemption changes the
verdict without touching the lockfile (replay R3 below).
- **Otherwise, both lockfiles are scanned.** The same pinned action
scans the PR's lockfile and the merge base's. Each is judged under its
own `osv-scanner.toml`, so the merge base's verdict is what `main`'s
scan gives and the PR's verdict is what the old absolute scan gave.
`scripts/osv-base-relative.mjs` then sorts the advisories:
- An advisory matched by both is **inherited**. It gets a `notice`
annotation that names it, the package versions each side matched, and
the open finding card that names it, when one exists. It does not fail
the step.
- An advisory matched only by the PR's lockfile is **introduced**. It
gets an `error` annotation and fails the step, as before.
- **The merge base.** `actions/checkout` puts the PR's test merge
(`refs/pull/N/merge`) at depth 1. Its first parent is the base-branch
commit that GitHub merged the PR into, which makes it the merge base of
the scanned tree. The step reads that parent from the commit object and
fetches it with one depth-1 `git fetch` (never `fetch-depth: 0`). If
HEAD is not a two-parent merge, the step fails with an error annotation.
- **Fail-closed.** A side is read only when its result file parses and
its step outcome matches its finding count (success means zero findings,
failure means some). In every other case the step fails and names that
side.

This adds no new gate, changes nothing that `main`'s scan judges, and
adds no exemption. `osv-scanner.toml`,
`scripts/check-osv-exemptions.mjs` and the other gate scripts are
untouched, and they still run first and unchanged.

## Why a helper script, and why not upstream's `osv-reporter-action`

The upstream reporter was read at `google/osv-scanner` v2.5.0
(`cmd/osv-reporter/main.go`,
`internal/ci/vulnerability_result_diff.go`). Three things rule it out:

- **It fails open.** When the new-side result file is missing or does
not parse, it logs a warning, treats the side as having no findings and
exits 0. Upstream's own reusable PR workflow runs both scans with
`continue-on-error: true`, so a scan error on the PR side passes the
gate.
- **It needs both sides at the same source path.** The diff keys on
`source.path`, so the base lockfile would have to be swapped into the
workspace and restored mid-job.
- **It cannot name the inherited advisories**, and the ruling requires
that notice.

The helper is one definition of introduced versus inherited, with the
scan-error guard built in. Its `--self-test` (23 cases, floor pinned)
runs as the first line of the stage step, so `check:self-test-wired`
sees it. The helper is in the `paths:` filter, so a PR that edits it
runs this workflow.

## Measured mechanism (osv-scanner 2.5.0, the binary in the pinned
action's image)

- **The pin.** `f4cfcc01edc9c8b756a9b873b7a623ca674da51e` is an untagged
upstream commit between the v2.5.0 and v2.5.1 tags. Its `action.yml`
runs `ghcr.io/google/osv-scanner-action:v2.5.0`, and run 37483796939's
job steps include "Pull ghcr.io/google/osv-scanner-action:v2.5.0". The
`# v2.3.8` comment had been stale since Dependabot's sha bump (objectstack-ai#9209).
It is corrected, and all three `uses:` lines stay on that one sha.
- **The image's binary matches the release binary.** It is built from
the same `cmd/osv-scanner` with the same flags (goreleaser builds
`osv-scanner-action`). The release binary `osv-scanner_linux_amd64`
matched its published SHA256SUMS (`edcfc41d…`). The action's entrypoint
is upstream's `exit_code_redirect.sh`, which rewrites exit 128 (no
packages) to 0.
- **The ledger is found only in the lockfile's own directory.** With a
ledger one directory up, an ignored advisory was still reported. The
workflow therefore passes `--config` explicitly on both sides. This
settles dispatch assumption ②: without `--config`, a base lockfile
staged at another path is scanned with no ledger at all.
- **Exit codes, with `--config` and `--output-file`:**

| case | exit | result file |
|:--|:--|:--|
| no findings | 0 | written, `results: []` |
| findings | 1 | written |
| expired `ignoreUntil` | 1 | written; the advisory is reported again
(the ledger's expiry convention holds on both sides) |
| unknown key / broken TOML / missing `--config` file | 127 | **none** |
| missing lockfile | 127 | **none** |
| no packages | 128, rewritten to 0 by the action | **none** |

Because the outcome alone cannot tell findings from errors, the helper's
file-plus-outcome agreement check is what keeps the verdict closed.

## The pin

The body is written once, when the PR is created and before any run
exists, so the run ids are posted in the `os-dev-report` comment on
objectstack-ai#22082, keyed by the head shas named here.

**Real runs** (seat amendment, from the dev's report on objectstack-ai#22082). All are
`validate-deps.yml` on `pull_request`, against the merge base
`ef1fcb26a24f` (the `main` tip):

| Head | Run (job) | Result |
|---|---|---|
| `ce598b1ab7` | `37719042481` (`113122177520`) | success, with the
"lockfile unchanged" notice |
| `501569be49` (probe: `minimist` 1.2.5) | `37719190017`
(`113122655549`) | **failure**, one error: `GHSA-xvch-5gv4-984h`
(minimist@1.2.5) |
| `3576cdc334` (probe reverted) | `37719368398` (`113123226526`) |
success, with the "lockfile unchanged" notice |
| `5473cad514` (probe: `minimist` 1.2.8, no advisory, lockfile changed)
| `37719558844` (`113123838149`) | success, with **six inherited
notices** for `next@16.3.6` |
| `cc98faf47a` (final; same tree as `ce598b1ab7`) | `37719733046`
(`113124391631`) | success, with the "lockfile unchanged" notice |

`main` at `ef1fcb26a24f` carries those six advisories (filed as objectstack-ai#22148).
So:
- the inherited pin is a **real run**: `37719558844`;
- the `scripts`-only pin is `37719368398` / `37719733046`, against that
advisory-carrying `main`;
- the replays R1–R3 below are supplementary.

### "A lockfile that introduces a new advisory fails": real run

- **Head `501569be49`** is a TEMPORARY probe commit. It adds root
devDependency `minimist` 1.2.5 (GHSA-xvch-5gv4-984h, fixed in 1.2.6)
through `pnpm add -D -w`. The PR squashes, and `3576cdc334` reverts the
probe.
- **Expected run result:** the stage step reports `compare=true`, both
scans run, and the judge fails with exactly one `error` annotation, for
GHSA-xvch-5gv4-984h (minimist@1.2.5).
- **Pre-check run locally**, with the same steps against main
`8fc50b7647` and the 2026-10-06 database snapshot described under R1–R3:

```
::error title=OSV advisory introduced by this pull request::GHSA-xvch-5gv4-984h (minimist@1.2.5) is matched by this pull request's lockfile and not by the merge base's (8fc50b7). ...
OSV base-relative verdict against the merge base (8fc50b7): 1 introduced, 0 inherited, 0 resolved.
judge exit=1
```

### "Lockfile unchanged": real runs

- The heads are `ce598b1ab7` (the implementation, at PR open) and
`3576cdc334` (after the probe's revert). The two trees are identical:
both are `d477febb20`.
- This PR changes neither `pnpm-lock.yaml` nor `osv-scanner.toml`, and
`.github/workflows/validate-deps.yml` is in the `paths:` filter, so the
workflow runs on it.
- **Expected:** a pass with the notice "lockfile unchanged — inherits
main's verdict", and the three scan and judge steps skipped.
- This branch runs no scan, so `main`'s advisory state cannot reach it.
That is why a `scripts`-only PR against a `main` that carries an
advisory passes here. R1 replays exactly that case.

### "Inherited passes with a notice": real run `37719558844` (above);
the replays below are supplementary

`main` is clean today: scheduled run 37565270565 succeeded. A real PR
run here cannot have a merge base that carries an advisory unless `main`
is touched or a throwaway branch is made, and both are out of bounds.
Following the dispatch, the step logic was replayed on real historical
pairs instead. The replay worked like this:

- It built the test merge GitHub would build (`git merge-tree
--write-tree BASE PRHEAD`, then `git commit-tree -p BASE -p PRHEAD`) in
an isolated clone.
- It ran the stage step's `run:` text verbatim, as extracted from this
branch's workflow. The only change was the helper's path, because the
historical trees carry no copy of the helper.
- It ran each scan step's `scan-args` verbatim through upstream's own
`exit_code_redirect.sh` and the release binary, with outcome = exit 0 ?
success : failure.
- It ran the judge step's `run:` text verbatim.

**The gap, named:** this container's egress policy refuses
`api.osv.dev`. The scans therefore ran `--offline` against a local
snapshot of the OSV npm database. That snapshot is an `all.zip` of
230,017 records, with max `modified` 2026-10-06T16:30:04Z and sha256
`0bd50081c140…`. It was found already on this container's disk, and its
provenance is otherwise unknown. Its GHSA-wq5f-xc86-pv6w (sharp,
published 13:43Z) and GHSA-pqg4-j6r4-53mv (shell-quote, published
13:40Z) records are those finding card objectstack-ai#22013 measured. The anchor
lookup used the real repo-scoped listing.

**R1: PR objectstack-ai#22002 as run 37483796939 saw it (the card's specimen: a
`scripts`-only change).** Base is `f0022c46c1`, `main`'s tip when the
run was created (15:00:57Z). PR head is `95c510eb8a`.

```
osv-base-relative self-test: 23 case(s), all held (floor 23)
::notice title=OSV-Scanner verdict (base-relative)::lockfile unchanged — inherits main's verdict. pnpm-lock.yaml and osv-scanner.toml are identical to the merge base f0022c4, so this pull request introduces no advisory; main's daily scheduled scan judges that lockfile.
GITHUB_OUTPUT: base=f0022c46c10a142f5fb29e6891a4fc06ec05a296 / compare=false
-- for contrast, the old absolute step on the same test merge's lockfile:
| https://osv.dev/GHSA-wq5f-xc86-pv6w | 8.9  | npm | sharp       | 0.35.4 | 0.35.5 | pnpm-lock.yaml |
| https://osv.dev/GHSA-pqg4-j6r4-53mv | 9.2  | npm | shell-quote | 1.10.0 | 1.11.0 | pnpm-lock.yaml |
Exit code: 1
```

**R2: PR objectstack-ai#21951 (a real lockfile-changing PR against a `main` that
carries advisories).** Base is `4c49150e0c`, PR head is `539b0752cd`.
The constructed merge's `pnpm-lock.yaml`, `osv-scanner.toml` and
`pnpm-workspace.yaml` blobs equal those of the PR's real squash commit
`787104baa9`.

```
GITHUB_OUTPUT: base=4c49150e0cb27a493390c4ac24e0b93e13efe662 / compare=true
osv-scan-base: found 1372 packages; GHSA-hp3w-g68c-fv3c filtered (the base's ledger); Exit code: 1  -> outcome failure
osv-scan-head: found 1371 packages; GHSA-hp3w-g68c-fv3c filtered (the PR's ledger);  Exit code: 1  -> outcome failure
::notice title=OSV advisory inherited from the merge base::GHSA-pqg4-j6r4-53mv (here: shell-quote@1.10.0; at the merge base: shell-quote@1.10.0) is matched by the merge base's lockfile (4c49150) too, so it is main's to fix and does not fail this pull request; no open issue names it yet; main's daily scheduled scan is the signal that files its finding card.
::notice title=OSV advisory inherited from the merge base::GHSA-wq5f-xc86-pv6w (here: sharp@0.35.4; at the merge base: sharp@0.35.4) is matched by the merge base's lockfile (4c49150) too, so it is main's to fix and does not fail this pull request; no open issue names it yet; main's daily scheduled scan is the signal that files its finding card.
OSV base-relative verdict against the merge base (4c49150): 0 introduced, 2 inherited, 3 resolved.
  resolved    GHSA-238p-pmpm-9mq7  katex@0.16.47
  resolved    GHSA-68fv-2mgg-jv7q  source-map-js@1.2.1
  resolved    GHSA-jqcg-44mw-7w3h  proxy-addr@2.0.7
judge exit=0
-- the old absolute step on the same lockfile: sharp + shell-quote, Exit code: 1
```

The anchor clause reads "no open issue names it yet" because objectstack-ai#22013 is
closed today. A live probe of the same listing found the open issues
that name a given advisory id. It also turned up a seat board post
quoting one, which is why board posts (`pm:seat`) are skipped; the
self-test pins both the anchor case and the board-post skip.

**R3: a synthetic ledger-only PR** that deletes the sprintf-js exemption
from `787104baa9`'s `osv-scanner.toml` and touches no lockfile:

```
GITHUB_OUTPUT: base=787104baa9ecca77ff13a8e83a4b7349728feadc / compare=true   (no short-circuit: the ledger differs)
::error title=OSV advisory introduced by this pull request::GHSA-hp3w-g68c-fv3c (sprintf-js@1.1.3) is matched by this pull request's lockfile and not by the merge base's (787104b). ...
OSV base-relative verdict against the merge base (787104b): 1 introduced, 2 inherited, 0 resolved.
judge exit=1
```

## Helper ablations (each run with `scripts/ablation-replace.mjs`, which
restores the file and confirms the restored blob equals HEAD)

- **A1: `compare` ignores the base** (`if (base.has(id)) inherited.push`
becomes `if (false) inherited.push`). The self-test fails: 25 failed
expectations over 23 cases, exit 1.
- **A2: a missing result file read as "no findings"** (upstream's
fail-open). The self-test fails: 4 failed expectations, exit 1.
- **Restore:** the blob is back to `f35d31ee1072` = HEAD, and `git diff
HEAD` is empty.

## Local verification

These gates ran at `bf15ed3831`, plus `ce598b1ab7` for the entry-guard
fix. The final tree `d477febb20` is identical at `ce598b1ab7` and
`3576cdc334`.

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands`
derived 54 commands over the actual diff, and all 54 exit 0.
- **One red caught and fixed:** `check:entry-guard` first went red
because the helper exported bindings and ran on import. The fix puts the
dispatch behind `isEntrypoint`, and the gate is now green.
- **`pnpm check:pm-dispatch-gates`:** 1976 cases pass, in 820.5s.
- **`--ran` reconciliation:** 54 derived, 54 run, 0 NOT-MEASURED.
- **eslint on the new file:** `npx eslint --no-inline-config --format
json scripts/osv-base-relative.mjs` reported 1 file, 0 errors and 0
warnings.
- The file is in eslint's population: a JSON result came back for it,
not an "ignored" warning.
- The config enables no type-aware linting (`eslint.config.mjs` has no
`parserOptions.project`), so this diff cannot change the verdict on any
file it does not touch.
  - The whole-repo `pnpm lint` belongs to CI.

## Acceptance notes

- **The unit is the advisory id,** following the ruling's wording ("an
advisory matched by both is reported as inherited"). Suppose a PR adds a
second vulnerable version of a package for an advisory `main` already
carries. That advisory reads as inherited, and the notice lists both
sides' package versions so the addition is visible. `main`'s daily scan
still judges it. Upstream's reporter counts occurrences instead.
- **Prose that drifts with this change, in files outside this PR's
allowed surface.** Both are out of bounds for this PR, and this PR has
no carrier for them:
- The `osv-scanner.toml` header says the workflow "blocks on any
advisory at any severity". That is still true of `main`'s scan, but a PR
now answers only for what it introduces.
- The `osv-scanner.toml` and `scripts/check-osv-exemptions.mjs` headers
cite measurements "against v2.3.8". The pinned image has been v2.5.0
since the Dependabot bump.
- **Remaining inherited-red path, by design:** an exemption that expires
on `main` still reddens every dependency-touching PR through the
unchanged "Verify OSV exemptions carry an expiry and a reason" step,
which the ruling keeps as is.
- **The job keeps `issues: write`,** which no step in it uses. The
judge's anchor lookup only reads. Permissions are unchanged here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants