Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package git-bug for openSUSE:Factory checked 
in at 2026-10-03 20:13:37
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/git-bug (Old)
 and      /work/SRC/openSUSE:Factory/.git-bug.new.1631729 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "git-bug"

Sat Oct  3 20:13:37 2026 rev:18 rq:1382186 version:0.11.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/git-bug/git-bug.changes  2026-09-08 
17:15:01.710392118 +0200
+++ /work/SRC/openSUSE:Factory/.git-bug.new.1631729/git-bug.changes     
2026-10-03 20:14:39.441425609 +0200
@@ -1,0 +2,57 @@
+Thu Sep 24 12:00:23 UTC 2026 - Matej Cepl <[email protected]>
+
+- Enable the Web UI RPM build by default on x86_64, retaining
+  an explicit CLI-only fallback and CLI builds elsewhere.
+  - Add the manual node_modules service and its legacy source
+    container and spec include.
+  - Install locked frontend dependencies through the local
+    registry, use Factory esbuild, and embed the Vite output
+    with the webui Go tag.
+- Require Go 1.26 and disable Go toolchain/module downloads.
+- Replace ignored test failures with offline handler and HTTP
+  smoke checks.
+- Install available third-party license notices and declare
+  a conservative frontend License expression.
+- Validate both RPM variants in local network-isolated builds.
+  Native-tool source policy, dependency pruning, missing upstream
+  notices, and clean OBS validation remain outstanding.
+
+-------------------------------------------------------------------
+Thu Sep 24 10:59:34 UTC 2026 - Matej Cepl <[email protected]>
+
+- Add preparatory Web UI source tooling: convert the pnpm lock
+  into a verified obs-service-node_modules inventory, generate
+  a separate npm installation lock in a network-isolated
+  environment, and add unit and HTTP smoke tests. Document the
+  successful x86-64 baseline using Factory esbuild. Web UI
+  enablement in the spec remains pending native-tool source
+  builds, dependency pruning, license review, and OBS
+  integration.
+
+-------------------------------------------------------------------
+Thu Sep 24 08:07:36 UTC 2026 - Matej Cepl <[email protected]>
+
+- Update to 0.11.0:
+  Sixteen months and close to 300 commits after v0.10.1, git-bug
+  v0.11.0 is available. This release is about two things. The
+  obvious one is a new web UI, rewritten from scratch and far
+  more capable than the one it replaces. The less obvious one is
+  a deep pass on correctness, which closes a handful of long
+  standing bugs.
+  - A new and more powerful web UI
+  - A Code Browser
+  - Correctness
+  - Forward compatibility
+  - A specification
+  - Breaking changes
+  - Web UI
+  - Core
+  - CLI
+  - Terminal UI
+  - API
+  - Bridges
+  - Documentation
+- Remove upstreamed patches:
+  - remote-config.patch
+
+-------------------------------------------------------------------

Old:
----
  git-bug-0.10.1.tar.gz
  remote-config.patch

New:
----
  WEBUI-PACKAGING.md
  collect-webui-licenses.py
  git-bug-0.11.0.tar.gz
  node-sources.json
  node_modules.obscpio
  node_modules.spec.inc
  package-lock.json
  pnpm-to-obs.py
  prepare-webui-lock.py
  test-webui.py
  test_collect_webui_licenses.py
  test_pnpm_to_obs.py
  test_prepare_webui_lock.py
  webui-lock-report.json
  webui-package.json

----------(Old B)----------
  Old:- Remove upstreamed patches:
  - remote-config.patch
----------(Old E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ git-bug.spec ++++++
--- /var/tmp/diff_new_pack.Plrrgj/_old  2026-10-03 20:15:35.002749966 +0200
+++ /var/tmp/diff_new_pack.Plrrgj/_new  2026-10-03 20:15:35.004750049 +0200
@@ -16,25 +16,56 @@
 #
 
 
+# The npm native build tools have only been validated on x86_64. Keep the
+# CLI available elsewhere; enable other architectures after testing them.
+%ifarch x86_64
+%bcond_without webui
+%else
+%bcond_with webui
+%endif
+
 Name:           git-bug
-Version:        0.10.1
+Version:        0.11.0
 Release:        0
 Summary:        Distributed, offline-first bug tracker embedded in git, with 
bridges
+%if %{with webui}
+# Conservative union of the frontend production dependency licenses. The
+# notices collector includes dependencies even when Vite tree-shakes them.
+License:        0BSD AND Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND 
BlueOak-1.0.0 AND CC-BY-4.0 AND ISC AND MIT AND OFL-1.1 AND Python-2.0 AND 
Unlicense
+%else
 License:        MIT
+%endif
 URL:            https://github.com/MichaelMure/git-bug
 Source0:        
https://github.com/MichaelMure/%{name}/archive/refs/tags/v%{version}.tar.gz#/git-bug-%{version}.tar.gz
 # Source0:        git-bug-%%{version}.tar.gz
 Source1:        vendor.tar.gz
-# PATCH-FIX-UPSTREAM remote-config.patch gh#MichaelMure/git-bug!1076 
[email protected]
-# try reading git-bug.remote config value before defaulting to 'origin' when 
no explicit REMOTE argument
-Patch0:         remote-config.patch
-BuildRequires:  golang(API) = 1.24
-# # PATCH-FEATURE-UPSTREAM 501-export.patch gh#MichaelMure/git-bug!501 
[email protected]
-# # add a command to export bugs as raw operations
-# Patch0:         501-export.patch
-BuildRequires:  golang-packaging
+Source2:        package-lock.json
+Source3:        webui-package.json
+Source4:        node_modules.spec.inc
+Source5:        test-webui.py
+Source6:        node-sources.json
+Source7:        pnpm-to-obs.py
+Source8:        prepare-webui-lock.py
+Source9:        test_pnpm_to_obs.py
+Source10:       test_prepare_webui_lock.py
+Source11:       webui-lock-report.json
+Source12:       WEBUI-PACKAGING.md
+Source13:       collect-webui-licenses.py
+Source14:       test_collect_webui_licenses.py
 BuildRequires:  git
-BuildRequires:  golang(API) >= 1.25
+BuildRequires:  golang(API) >= 1.26
+BuildRequires:  golang-packaging
+%if %{with webui}
+# OBS/osc unpacks node_modules.obscpio into the source directory. This include
+# declares its npm tarballs as individual sources for the source RPM.
+%include %{_sourcedir}/node_modules.spec.inc
+# The npm esbuild wrapper requires an executable with the identical version.
+BuildRequires:  esbuild = 0.28.2
+BuildRequires:  local-npm-registry >= 1.1.0
+BuildRequires:  nodejs24
+BuildRequires:  npm24
+BuildRequires:  python3-base
+%endif
 
 %description
 git-bug is a bug tracker that:
@@ -90,14 +121,39 @@
 
 %prep
 %autosetup -p1 -a1
+%if %{with webui}
+# Use the npm-generated installation tree, not the service download inventory.
+cp %{SOURCE2} webui/package-lock.json
+cp %{SOURCE3} webui/package.json
+%endif
 
 %build
-# COMMANDS_PATH="github.com/git-bug/git-bug/commands"
-# LDFLAGS="-X ${COMMANDS_PATH}.GitCommit=${GIT_COMMIT} \
-#      -X ${COMMANDS_PATH}.GitLastTag=${GIT_LAST_TAG} \
-#      -X ${COMMANDS_PATH}.GitExactTag=${GIT_EXACT_TAG}"
-export GOFLAGS="-buildmode=pie"
+export GOTOOLCHAIN=local
+export GOPROXY=off
+export GOSUMDB=off
+export GOFLAGS="-mod=vendor -buildmode=pie"
+%if %{with webui}
+# Keep npm configuration and caches inside the build tree. All dependencies
+# come from source tarballs served over loopback; disable install scripts.
+export npm_config_cache="$PWD/.npm-cache"
+export npm_config_userconfig="$PWD/.npmrc"
+export npm_config_globalconfig="$PWD/.npmrc-global"
+export npm_config_update_notifier=false
+export NODE_OPTIONS="--max-old-space-size=1536"
+export ESBUILD_BINARY_PATH="$(command -v esbuild)"
+test -x "$ESBUILD_BINARY_PATH"
+pushd webui
+local-npm-registry %{_sourcedir} ci \
+    --include=dev --ignore-scripts --legacy-peer-deps \
+    --no-audit --no-fund --maxsockets=2
+npm run build
+test -s dist/index.html.gz
+popd
+python3 %{SOURCE13} webui webui-licenses
+go build -tags webui
+%else
 go build
+%endif
 
 %install
 install -Dm755 git-bug %{buildroot}%{_bindir}/git-bug
@@ -112,11 +168,24 @@
     %{buildroot}%{_sysconfdir}/zsh_completion.d/git-bug
 
 %check
-# before we mark network requiring tests (gh#git-bug/git-bug#1313)
-go test -v -bench=. ./... || true
+export GOTOOLCHAIN=local
+export GOPROXY=off
+export GOSUMDB=off
+export GOFLAGS="-mod=vendor -buildmode=pie"
+# The full suite includes network-dependent bridge tests 
(gh#git-bug/git-bug#1313).
+# Run the offline Web UI handler tests without suppressing failures.
+%if %{with webui}
+go test -v -tags webui ./webui
+python3 %{SOURCE5} ./git-bug
+%else
+go test -v ./webui
+%endif
 
 %files
 %license LICENSE
+%if %{with webui}
+%license webui-licenses
+%endif
 %doc README.md
 %{_bindir}/git-bug
 %{_mandir}/man1/git*.1%{?ext_man}

++++++ WEBUI-PACKAGING.md ++++++
# Building the git-bug Web UI for openSUSE

## Status

The spec now builds and embeds the Web UI by default on x86-64. Other
architectures retain the CLI-only build until their native frontend tools are
validated. Use `--without webui` for the CLI-only variant or `--with webui` to
explicitly test the frontend on another architecture.

`_service` generates `node_modules.obscpio` and `node_modules.spec.inc` for the
traditional OSC workflow. Both the Web UI-enabled and CLI-only RPM builds have
passed locally with external networking disabled.

This proof used upstream npm native binaries for Rolldown, Oxc, Tailwind, and
Lightning CSS. Only esbuild was replaced with the matching Factory executable.
A successful baseline is not proof that the package meets source-build policy
or supports every openSUSE architecture.

| Plan step | Status |
|---|---|
| 1. Separate downloading and npm resolution | Complete |
| 2. Implement converter | Complete |
| 3. Use service's multi-lock input | Complete and tested with installed 
service |
| 4. Test conversion | Complete; 27 unit tests across preparation and notice 
collection |
| 5. Generate genuine npm lock | Generated; provenance checked and fresh-cache 
installation passed. Exact peer-graph equivalence is not claimed. |
| 6. Integrate service/submission workflow | Implemented with a manual service, 
legacy cpio container, and generated source include |
| 7. Factory tools and native dependencies | Factory esbuild validated; 
remaining native tooling unresolved |
| 8. Reduce dependencies | Pending; baseline established |
| 9. Update spec and source patches | Spec integration complete; no upstream 
source patch required |
| 10. Offline checks and packaging review | Both local RPM variants passed; 
packaged Web UI binary passed smoke tests. Clean OBS build and full license 
review pending |
| 11. Maintenance documentation | Service regeneration, RPM build, and fallback 
documented |

## Two different artifacts

- `node-sources.json` is a generated download inventory for
  `obs-service-node_modules`. It describes tarball URLs and checksums, not an
  installation layout. Never pass it to npm as `package-lock.json`.
- `package-lock.json` must be produced and validated by npm. It records the
  actual installation tree and peer resolution. It is not generated by the
  converter.

The upstream `webui/pnpm-lock.yaml` remains the authority for the initial source
inventory. Generating an npm lock must not silently fetch newer versions from
an external registry.

## Converter

Requirements: Python 3.8 or newer and PyYAML, available in openSUSE as the
appropriate `python3*-PyYAML` package for the interpreter. Tests use the 
standard
library. The optional installed-service integration test also uses that
service's own dependencies.

Run from the packaging directory, with the release unpacked separately:

```sh
python3 pnpm-to-obs.py \
    --input /path/to/git-bug-0.11.0/webui/pnpm-lock.yaml \
    --output node-sources.json

PYTHONDONTWRITEBYTECODE=1 python3 -m unittest -v \
    test_pnpm_to_obs.py test_prepare_webui_lock.py 
test_collect_webui_licenses.py
```

The converter supports pnpm lockfile version 9. It keeps all package versions
and platform variants, including optional native bindings. It preserves explicit
HTTPS tarball URLs and derives standard npm registry URLs for integrity-only
registry records. It does not read npm registry configuration or contact the
network.

The output uses the installed service's support for a mapping of npm-v3-shaped
objects. Each object contains one package identity. Peer-specific records with
the same source collapse into one download. Different versions remain distinct.

The converter rejects missing or malformed hashes, unsupported lock versions,
local/workspace/Git resolutions, patched dependencies, duplicate YAML keys,
conflicting source records, and service tarball filename collisions. Unsupported
inputs need explicit implementation, not an automatic fallback to current npm
registry contents.

### Download and verify

For a standalone download cache, the service can consume the inventory directly:

```sh
/usr/lib/obs/service/node_modules \
    --input node-sources.json \
    --node-dir /path/to/downloaded-tarballs \
    --download

python3 pnpm-to-obs.py \
    --input /path/to/git-bug-0.11.0/webui/pnpm-lock.yaml \
    --output node-sources.json \
    --verify-dir /path/to/downloaded-tarballs
```

Verification is required. The installed service can log a download error without
returning a failing exit status. The converter's verification checks that every
expected tarball exists and matches its recorded integrity hashes.

These downloads are source preparation. They must not happen during rpmbuild.

The configured manual service entry is:

```xml
<service name="node_modules" mode="manual">
  <param name="input">node-sources.json</param>
  <param name="legacy-container">true</param>
  <param name="cpio">node_modules.obscpio</param>
  <param name="output">node_modules.spec.inc</param>
  <param name="source-offset">10000</param>
</service>
```

After regenerating `node-sources.json`, update the submitted sources with:

```sh
osc service manualrun node_modules
```

Submit both `node_modules.obscpio` and `node_modules.spec.inc` along with the
manifest, npm lock, scripts, spec, and service definition. The cpio container
currently occupies about 809 MiB. `.gitignore` explicitly allows this file;
the existing Git LFS attributes cover it.

OBS/osc expands the container into individual source tarballs before RPM runs.
The generated include declares them as sources numbered from 10000. The RPM
build uses these regular files from the source directory. Do not replace them
with symlinks in a local test: the registry helper skips symlink directory 
entries.

For a plain `rpmbuild` invocation outside OSC, first extract the container in
your RPM source directory:

```sh
cd /path/to/rpm/SOURCES
cpio -idm < /path/to/package/node_modules.obscpio
```

Use the extracted directory as `--verify-dir` for the converter and as
`--tarballs` for the npm-lock preparation tool. Generating the installation lock
is a maintainer operation; the spec only consumes the submitted lock.

## Generate the genuine npm lock

`prepare-webui-lock.py` automates this stage. It uses an empty temporary cache,
pins direct dependencies to their upstream importer versions, translates the
reviewed pnpm override to npm, and starts `local-npm-registry` with the verified
tarballs. It disables scripts and auditing and runs in a new Linux user/network
namespace with only loopback enabled. It requires `unshare` and permission to
create unprivileged user namespaces.

```sh
python3 prepare-webui-lock.py \
    --webui /path/to/git-bug-0.11.0/webui \
    --tarballs /path/to/downloaded-tarballs \
    --output-dir .
```

The outputs are:

- `webui-package.json`, the npm-compatible manifest with pinned direct versions.
- `package-lock.json`, a genuine npm-generated installation lock.
- `webui-lock-report.json`, the selected source count and unused source 
identities.

The helper verifies the complete tarball inventory before starting npm. It
rejects selected sources or integrity hashes absent from the pnpm inventory.
For dependencies bundled inside another npm tarball, it checks their manifests
inside the already verified parent archive. The current tree has six such
bundled entries, including versions not listed separately in the pnpm lock.

The override translation is deliberately specific to this release. If upstream
changes its overrides, the helper fails and requires review rather than guessing
how pnpm-specific selectors map to npm.

There is an important limitation in Factory's `local-npm-registry` 1.1.0:
its normal wrapper appends `--no-package-lock` to the npm command. Do not assume
that `local-npm-registry ... install --package-lock-only` will generate a lock.
The preparation tool uses the helper's `--debug` server mode and invokes npm
separately with its explicit localhost registry URL.

It passes `--omit-lockfile-registry-resolved`, so npm writes a portable lock
without temporary localhost URLs. The integrity hashes and installation tree
remain locked. The normal helper successfully installs this lock with `ci`,
despite appending `--no-package-lock`.

Use `--legacy-peer-deps` consistently. Strict resolution repeatedly reported
incompatible upstream TypeScript peer requirements and was stopped after three
minutes. Legacy resolution completed in four seconds. This choice does not
prove that npm reproduces pnpm's peer graph.

Any generated lock must be checked for:

- All selected package versions belonging to the verified source inventory.
- Peer-resolution changes relative to pnpm snapshots.
- Locally generated registry URLs that must not survive into a portable lock.
- Successful installation with a new empty cache and a fresh local registry.
- No omitted platform bindings needed on the intended build architectures.

### Validated baseline

The full inventory contains 1,167 verified tarballs, about 811 MiB on disk. npm
selected 1,041 distinct source identities for 1,089 installation entries, of
which six are bundled entries. On x86-64, `ci` installed 907 packages.

With the generated files copied into `webui/` as `package.json` and
`package-lock.json`, the tested installation command was equivalent to:

```sh
local-npm-registry /path/to/downloaded-tarballs ci \
    --ignore-scripts --legacy-peer-deps --no-audit --no-fund --maxsockets=2
```

The test used a fresh cache and a network namespace without external interfaces.
The local registry helper was the upstream 1.1.0 release referenced by Factory's
spec, unpacked in `/tmp`, rather than a system-installed RPM. The preparation
tool accepts `--registry-helper node /absolute/path/to/dist/index.js` for this
case. The final package should use Factory's `local-npm-registry` RPM.

Factory's signed `esbuild-0.28.2-1.1.x86_64.rpm` was verified and unpacked in a
temporary directory. `ESBUILD_BINARY_PATH` pointed at that executable during
`npm run build`. Vite successfully transformed 3,546 modules and generated
`webui/dist`, including compressed assets and Inter webfonts.

The generated assets were embedded with Go 1.26.8 using the supplied vendor
archive, `GOTOOLCHAIN=local`, `GOPROXY=off`, `GOSUMDB=off`, and
`GOFLAGS='-mod=vendor -buildmode=pie'`. Both the build and
`go test -tags webui ./webui` passed with external networking disabled.

Run the standalone smoke test against a built executable:

```sh
python3 test-webui.py /path/to/git-bug
```

It creates a temporary Git repository, starts a read-only server on localhost,
and checks HTML, a client-side route, gzip handling, referenced JS/CSS assets,
webfonts, and a GraphQL query. The baseline passed checks for 11 referenced
assets and seven fonts. This is an HTTP smoke test, not a browser interaction
test.

A trial using `file:` tarball URLs instead of the local registry caused an
OOM kill; a heap-limited retry did not complete within two minutes. That
transport is not used by the generated lock or recommended build commands.

## RPM integration and validation

The spec now:

- Requires Go 1.26 or newer, matching `go.mod`, instead of the contradictory
  Go 1.24 and 1.25 requirements.
- Uses Factory Node 24, npm 24, the local registry helper, Python for checks,
  and esbuild 0.28.2 only when Web UI support is enabled.
- Copies the npm-compatible manifest and lock into `webui/`, installs through
  the loopback registry with lifecycle scripts disabled, and runs the Vite 
build.
- Uses the vendored Go dependencies and disables Go toolchain/module downloads.
- Embeds the generated assets with `go build -tags webui`.
- Collects available production dependency notices and installs them as license
  files. The License expression conservatively covers the production dependency
  inventory, including dependencies that tree shaking may remove.
- Runs handler tests and the HTTP smoke test without suppressing failures. The
  broader bridge suite remains excluded because it needs external services.

`collect-webui-licenses.py` checks license identifiers against the spec's 
declared
set and records the copied notices in `webui-licenses/index.json`. It reports
missing upstream notice files rather than inventing copyright notices. The
current archives for `@wry/context` and `@wry/trie` omit top-level license text;
their metadata and READMEs are retained. These omissions still need review.

The latest local validation ran the actual spec with `rpmbuild -bb --nodeps`,
inside a network namespace with only loopback enabled, for both the default
x86-64 variant and `--without webui`. Dependency checks were bypassed because
the registry helper and signed Factory esbuild RPM were staged locally rather
than installed system-wide. This is a full local RPM build, not a clean OBS
buildroot validation.

Both runs produced the main RPM and completion subpackages. The binary extracted
from the Web UI RPM passed the HTTP smoke test after RPM's stripping and 
packaging
steps. Its runtime requirements contain no Node.js, npm, or esbuild dependency.
The builds emitted an empty debugsource file-list warning, which remains to be
reviewed separately.

The generated lock and successful RPM builds do not settle native source-build
requirements, other architectures, or the complete frontend license audit.

## Remaining implementation stages

1. Review npm/pnpm graph differences beyond the verified source inventory;
   retain the explicit peer-resolution policy.
2. Prune development dependencies now that the full frontend baseline works,
   regenerate the npm lock, and reduce the service inventory to the required
   dependency closure without dropping necessary optional platform bindings.
3. Decide how to build native modules from source and which architectures can
   enable the UI. Do not treat npm binary tarballs as source-built tooling.
4. Complete the license/notice audit, including the two missing upstream license
   texts noted above.
5. Run a clean OBS build with dependency resolution enabled and review the
   debugsource warning.

Use Quilt for upstream source patches, registering files before editing them.
The packaging directory itself currently has no Quilt series.

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.Plrrgj/_old  2026-10-03 20:15:35.086753478 +0200
+++ /var/tmp/diff_new_pack.Plrrgj/_new  2026-10-03 20:15:35.089753604 +0200
@@ -1,5 +1,5 @@
-mtime: 1788765301
-commit: d87dd61b4cbb641fc2c79eed6b4f75e6a70a0855e1aa8666c0e60cad7dfed8c6
+mtime: 1790364639
+commit: 4918e1e1bf7b46b2e3826b6bd4bafee6f8fca331c7aff42cd84b803466444c8d
 url: https://src.opensuse.org/mcepl/git-bug
 revision: factory
 

++++++ _service ++++++
--- /var/tmp/diff_new_pack.Plrrgj/_old  2026-10-03 20:15:35.113754607 +0200
+++ /var/tmp/diff_new_pack.Plrrgj/_new  2026-10-03 20:15:35.116754733 +0200
@@ -14,17 +14,15 @@
     </service>
     <service name="set_version" mode="manual"/ -->
     <service name="go_modules" mode="manual">
-      <param 
name="replace">golang.org/x/crypto=golang.org/x/[email protected]</param>
-      <param name="replace">golang.org/x/net=golang.org/x/[email protected]</param>
-      <param name="replace">golang.org/x/text=golang.org/x/[email protected]</param>
-      <param 
name="replace">golang.org/x/oauth=golang.org/x/[email protected]</param>
-      <param 
name="replace">github.com/go-viper/mapstructure/v2=github.com/go-viper/mapstructure/[email protected]</param>
-      <!-- param 
name="replace">github.com/blevesearch/bleve=github.com/blevesearch/[email protected]</param
 -->
-      <param 
name="replace">github.com/cloudflare/circl=github.com/cloudflare/[email protected]</param>
-      <param 
name="replace">github.com/go-git/go-git/v5=github.com/go-git/go-git/[email protected]</param>
-      <param 
name="replace">golang.org/x/crypto/ssh=golang.org/x/crypto/[email protected]</param>
-      <param 
name="replace">golang.org/x/crypto/ssh/agent=golang.org/x/crypto/ssh/[email protected]</param>
-      <param 
name="replace">github.com/go-git/go-billy=github.com/go-git/go-billy/[email protected]</param>
+    </service>
+    <!-- Traditional osc workflow: archive the npm sources and declare each
+         tarball in the generated spec include. No build-time downloads. -->
+    <service name="node_modules" mode="manual">
+        <param name="input">node-sources.json</param>
+        <param name="legacy-container">true</param>
+        <param name="cpio">node_modules.obscpio</param>
+        <param name="output">node_modules.spec.inc</param>
+        <param name="source-offset">10000</param>
     </service>
 </services>
 

++++++ build.specials.obscpio ++++++
--- old/.gitignore      2026-09-07 09:15:01.000000000 +0200
+++ new/.gitignore      2026-09-25 21:30:39.000000000 +0200
@@ -2,6 +2,7 @@
 build.specials.obscpio
 git-bug/
 *.obscpio
+!node_modules.obscpio
 *.osc
 .osc
 .pbuild

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-09-25 21:30:39.000000000 +0200
@@ -0,0 +1,11 @@
+_build.*
+build.specials.obscpio
+git-bug/
+*.obscpio
+!node_modules.obscpio
+*.osc
+.osc
+.pbuild
+_scmsync.obsinfo
+_service:*
+git-bug-*-build/

++++++ collect-webui-licenses.py ++++++
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Collect available notices for installed non-dev npm dependencies.

This deliberately includes more packages than Vite may retain after tree
shaking. Missing upstream license files are reported, not invented.
"""
import argparse
import json
from pathlib import Path
import shutil
import sys


def collect(webui, destination):
    lock = json.loads((webui / "package-lock.json").read_text())
    index = []
    # Keep this in sync with the conservative License expression in the spec.
    declared = {"0BSD", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", 
"BlueOak-1.0.0",
                "CC-BY-4.0", "ISC", "MIT", "OFL-1.1", "Python-2.0", "Unlicense"}
    for path, package in sorted(lock["packages"].items()):
        if not path or package.get("dev"):
            continue
        relative = Path(path)
        if relative.is_absolute() or ".." in relative.parts or 
relative.parts[0] != "node_modules":
            raise ValueError(f"Invalid package path: {path}")
        source = webui / relative
        if not source.is_dir():
            # Optional packages for other platforms are not installed.
            if package.get("optional"):
                continue
            raise ValueError(f"Missing installed package: {path}")
        if package.get("license") not in declared:
            raise ValueError(f"Review the spec License expression for {path}: 
{package.get('license')}")
        target = destination / relative
        target.mkdir(parents=True, exist_ok=True)
        shutil.copyfile(source / "package.json", target / 
"package-metadata.json")
        notices = []
        for item in sorted(source.iterdir()):
            if item.is_file() and item.name.lower().startswith(
                    ("license", "licence", "copying", "notice", "copyright", 
"ofl")):
                shutil.copyfile(item, target / item.name)
                notices.append(item.name)
        if not notices:
            print(f"warning: no top-level license text supplied by {path}", 
file=sys.stderr)
            for item in source.glob("README*"):
                if item.is_file():
                    shutil.copyfile(item, target / item.name)
        index.append({"path": path, "version": package["version"],
                      "license": package.get("license", "UNKNOWN"), "notices": 
notices})
    (destination / "index.json").write_text(json.dumps(index, indent=2) + "\n")
    print(f"Collected available notices for {len(index)} installed production 
dependencies")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("webui", type=Path)
    parser.add_argument("destination", type=Path)
    args = parser.parse_args()
    collect(args.webui, args.destination)

++++++ git-bug-0.10.1.tar.gz -> git-bug-0.11.0.tar.gz ++++++
++++ 133620 lines of diff (skipped)

++++++ node-sources.json ++++++
++++ 11673 lines (skipped)

++++++ node_modules.spec.inc ++++++
++++ 1168 lines (skipped)

++++++ package-lock.json ++++++
++++ 15300 lines (skipped)

++++++ pnpm-to-obs.py ++++++
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Convert a pnpm v9 lock to an obs-service-node_modules download inventory.

Requires Python 3 and PyYAML. No network access, dependency resolution, or
installation is performed. The output is NOT an npm installation lockfile.
"""

import argparse
import base64
import binascii
import hashlib
import json
import re
import sys
from pathlib import Path
from urllib.parse import quote, urlsplit

import yaml


class ConversionError(ValueError):
    """Input cannot be represented safely as a service download inventory."""


class UniqueKeyLoader(yaml.SafeLoader):
    """Do not silently overwrite conflicting YAML keys."""


def unique_mapping(loader, node, deep=False):
    result = {}
    for key_node, value_node in node.value:
        key = loader.construct_object(key_node, deep=deep)
        try:
            if key in result:
                raise ConversionError(f"Duplicate YAML key: {key!r}")
            result[key] = loader.construct_object(value_node, deep=deep)
        except TypeError as exc:
            raise ConversionError("YAML mapping keys must be scalar values") 
from exc
    return result


UniqueKeyLoader.add_constructor(
    yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, unique_mapping
)
NAME = r"(?:@[a-z0-9_~][a-z0-9._~-]*/)?[a-z0-9_~][a-z0-9._~-]*"
NUMBER = r"(?:0|[1-9][0-9]*)"
VERSION = 
rf"{NUMBER}\.{NUMBER}\.{NUMBER}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?"
PACKAGE = 
re.compile(rf"^(?P<name>{NAME})@(?P<version>{VERSION})(?P<peers>\(.*\))?$")
DIGEST_SIZES = {"sha1": 20, "sha256": 32, "sha384": 48, "sha512": 64}
UNSUPPORTED = ("link:", "workspace:", "file:", "git:", "git+", "github:", 
"http:", "https:")


def mapping(value, context):
    if not isinstance(value, dict):
        raise ConversionError(f"{context}: expected a mapping")
    return value


def integrity_value(value, context):
    if not isinstance(value, str) or not value.split():
        raise ConversionError(f"{context}: missing integrity hash")
    algorithms = {}
    for token in value.split():
        algorithm, separator, encoded = token.partition("-")
        if not separator or algorithm not in DIGEST_SIZES:
            raise ConversionError(f"{context}: unsupported integrity algorithm")
        try:
            digest = base64.b64decode(encoded, validate=True)
        except (ValueError, binascii.Error) as exc:
            raise ConversionError(f"{context}: malformed integrity hash") from 
exc
        if len(digest) != DIGEST_SIZES[algorithm]:
            raise ConversionError(f"{context}: incorrect {algorithm} digest 
length")
        if algorithm in algorithms and algorithms[algorithm] != digest:
            raise ConversionError(f"{context}: conflicting {algorithm} 
integrity hashes")
        algorithms[algorithm] = digest
    return " ".join(sorted(set(value.split())))


def https_url(value, context):
    if not isinstance(value, str) or any(c.isspace() for c in value):
        raise ConversionError(f"{context}: expected an HTTPS tarball URL")
    try:
        url = urlsplit(value)
        valid = (url.scheme == "https" and url.hostname
                 and url.path.rsplit("/", 1)[-1] not in {"", ".", ".."}
                 and not url.username and not url.password and not url.fragment)
        _ = url.port
    except ValueError as exc:
        raise ConversionError(f"{context}: malformed URL") from exc
    if not valid:
        raise ConversionError(f"{context}: expected HTTPS without credentials 
or fragment")
    return value


def service_filename(url):
    """Match obs-service-node_modules' make_unique_fn_from_path convention."""
    parts = urlsplit(url).path.split("/")
    basename = parts[-1]
    prefix = [part for part in parts[1:-1]
              if part != "-" and not basename.startswith(part)]
    return "-".join([*prefix, basename])


def check_references(lock):
    # Local/workspace dependencies may occur only in importers, with no package
    # record. Refuse these instead of producing an apparently complete 
inventory.
    for section in ("importers", "snapshots"):
        for key, record in mapping(lock.get(section, {}), section).items():
            record = mapping(record, f"{section}.{key}")
            for kind in ("dependencies", "devDependencies", 
"optionalDependencies"):
                for name, reference in mapping(record.get(kind, {}), 
f"{key}.{kind}").items():
                    if isinstance(reference, dict):
                        reference = reference.get("version")
                    if not isinstance(reference, str):
                        raise ConversionError(f"{key}: invalid reference for 
{name}")
                    if reference.startswith(UNSUPPORTED) or "patch_hash=" in 
reference:
                        raise ConversionError(f"{key}: unsupported dependency 
{name}: {reference}")


def convert(lock):
    lock = mapping(lock, "lockfile")
    if str(lock.get("lockfileVersion")) != "9.0":
        raise ConversionError("Only pnpm lockfileVersion 9.0 is supported")
    if lock.get("patchedDependencies"):
        raise ConversionError("patchedDependencies require explicit patch 
handling")
    check_references(lock)
    packages = mapping(lock.get("packages"), "packages")
    if not packages:
        raise ConversionError("The package inventory is empty")
    output = {}
    by_url = {}
    by_filename = {}
    for key, metadata in packages.items():
        match = PACKAGE.fullmatch(key) if isinstance(key, str) else None
        if not match:
            raise ConversionError(f"Unsupported registry package identity: 
{key!r}")
        name, version, peers = match.group("name", "version", "peers")
        if peers:
            depth = 0
            for char in peers:
                depth += (char == "(") - (char == ")")
                if depth < 0:
                    break
            if depth != 0 or "patch_hash=" in peers:
                raise ConversionError(f"Unsupported peer/patch suffix: {key}")
        metadata = mapping(metadata, key)
        if metadata.get("patched"):
            raise ConversionError(f"{key}: patched package is unsupported")
        resolution = mapping(metadata.get("resolution"), f"{key}.resolution")
        unknown = set(resolution) - {"integrity", "tarball"}
        if unknown:
            raise ConversionError(f"{key}: unsupported resolution fields: 
{sorted(unknown)}")
        integrity = integrity_value(resolution.get("integrity"), key)
        url = resolution.get("tarball")
        if url is None:
            basename = name.rsplit("/", 1)[-1]
            url = (f"https://registry.npmjs.org/{quote(name, safe='@/')}/-/"
                   f"{quote(basename, safe='')}-{quote(version, safe='')}.tgz")
        url = https_url(url, key)
        identity = f"{name}@{version}"
        entry = {
            "lockfileVersion": 3,
            "packages": {
                f"node_modules/{name}": {
                    "version": version, "resolved": url, "integrity": integrity
                }
            },
        }
        if identity in output and output[identity] != entry:
            raise ConversionError(f"Conflicting sources for {identity}")
        if url in by_url and by_url[url] != integrity:
            raise ConversionError(f"Conflicting integrity hashes for {url}")
        filename = service_filename(url)
        if filename in by_filename and by_filename[filename] != (url, 
integrity):
            raise ConversionError(f"Service tarball filename collision: 
{filename}")
        by_filename[filename] = (url, integrity)
        by_url[url] = integrity
        output[identity] = entry
    return dict(sorted(output.items()))


def verify_downloads(inventory, directory):
    """The service may log download errors without failing; check every file."""
    errors = []
    seen = set()
    for lock in inventory.values():
        entry = next(iter(lock["packages"].values()))
        filename = service_filename(entry["resolved"])
        if filename in seen:
            continue
        seen.add(filename)
        expected = dict(token.split("-", 1) for token in 
entry["integrity"].split())
        hashes = {algorithm: hashlib.new(algorithm) for algorithm in expected}
        try:
            with (directory / filename).open("rb") as stream:
                while chunk := stream.read(1024 * 1024):
                    for digest in hashes.values():
                        digest.update(chunk)
        except OSError as exc:
            errors.append(f"{filename}: {exc.strerror}")
            continue
        for algorithm, digest in hashes.items():
            if base64.b64encode(digest.digest()).decode("ascii") != 
expected[algorithm]:
                errors.append(f"{filename}: {algorithm} checksum mismatch")
    if errors:
        raise ConversionError("Download verification failed:\n" + 
"\n".join(errors))
    return len(seen)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--input", type=Path, required=True, 
help="pnpm-lock.yaml")
    parser.add_argument("--output", type=Path, required=True, 
help="service-only JSON inventory")
    parser.add_argument("--verify-dir", type=Path,
                        help="also verify every downloaded tarball in this 
directory")
    args = parser.parse_args()
    try:
        if args.input.resolve() == args.output.resolve():
            raise ConversionError("Input and output paths must differ")
        if args.output.name in {"package-lock.json", "npm-shrinkwrap.json"}:
            raise ConversionError("Use node-sources.json: output is NOT an npm 
install lock")
        lock = yaml.load(args.input.read_text(encoding="utf-8"), 
Loader=UniqueKeyLoader)
        result = convert(lock)
        if args.verify_dir is not None:
            count = verify_downloads(result, args.verify_dir)
            print(f"Verified {count} tarballs.", file=sys.stderr)
        content = json.dumps(result, indent=2, sort_keys=True, 
ensure_ascii=True) + "\n"
        args.output.write_text(content, encoding="utf-8")
    except (ConversionError, OSError, yaml.YAMLError) as exc:
        parser.exit(1, f"error: {exc}\n")
    print(f"Wrote {len(result)} package identities to {args.output}; service 
input only.", file=sys.stderr)


if __name__ == "__main__":
    main()

++++++ prepare-webui-lock.py ++++++
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Generate git-bug's npm manifest and lock using only verified pnpm sources.

Maintainer-side tool, not an rpmbuild step. Requires Python/PyYAML, Node/npm,
local-npm-registry, and Linux unshare with unprivileged user namespaces enabled.
The npm resolver runs in a new network namespace with only loopback enabled.
"""
import argparse
import fcntl
import json
import os
from pathlib import Path
import re
import runpy
import socket
import struct
import subprocess
import sys
import tarfile
import tempfile
import time

import yaml

CONVERTER = runpy.run_path(str(Path(__file__).with_name("pnpm-to-obs.py")))
Error = CONVERTER["ConversionError"]


def build_manifest(package, lock):
    """Pin direct dependencies; leave transitive resolution to npm."""
    result = json.loads(json.dumps(package))
    if set(lock.get("importers", {})) != {"."}:
        raise Error("Expected a single root importer for the git-bug Web UI")
    importer = lock["importers"]["."]
    for kind in ("dependencies", "devDependencies", "optionalDependencies"):
        records = importer.get(kind, {})
        if set(result.get(kind, {})) != set(records):
            raise Error(f"package.json and pnpm importer disagree on {kind}")
        for name, record in records.items():
            if record["specifier"] != result[kind][name]:
                raise Error(f"package.json and pnpm importer disagree on 
{name}")
            version = record["version"].split("(", 1)[0]
            if not re.fullmatch(CONVERTER["VERSION"], version):
                raise Error(f"Unsupported direct dependency reference: 
{name}={version}")
            result[kind][name] = version
    overrides = lock.get("overrides", {})
    if overrides != {"js-yaml@4": "^4.3.2"}:
        raise Error("Upstream overrides changed; review their npm translation")
    result["overrides"] = overrides
    result.pop("pnpm", None)
    result.pop("packageManager", None)
    return result


def validate_lock(lock, inventory, tarballs):
    """Validate source provenance, including dependencies bundled in tarballs.

    This does not assert identical npm and pnpm peer/dependency graphs.
    """
    if lock.get("lockfileVersion") != 3:
        raise Error("Expected an npm version-3 lock")
    selected = set()
    bundled_count = 0
    packages = lock["packages"]
    for path, entry in packages.items():
        if not path:
            continue
        name = entry.get("name", path.rsplit("node_modules/", 1)[-1])
        identity = f"{name}@{entry['version']}"
        if "resolved" in entry or entry.get("link"):
            raise Error(f"Non-portable URL or link in npm lock: {path}")
        if entry.get("inBundle"):
            parent = path.rsplit("/node_modules/", 1)[0]
            while packages.get(parent, {}).get("inBundle"):
                parent = parent.rsplit("/node_modules/", 1)[0]
            if parent == path or parent not in packages:
                raise Error(f"No source package for bundled entry: {path}")
            ancestor = packages[parent]
            parent_name = ancestor.get("name", parent.rsplit("node_modules/", 
1)[-1])
            source = inventory.get(f"{parent_name}@{ancestor['version']}")
            if source is None:
                raise Error(f"Unknown bundle parent: {parent}")
            source_entry = next(iter(source["packages"].values()))
            filename = CONVERTER["service_filename"](source_entry["resolved"])
            relative = path[len(parent) + 1:]
            if ".." in relative.split("/"):
                raise Error(f"Invalid bundled path: {path}")
            with tarfile.open(tarballs / filename) as archive:
                member = archive.getmember(f"package/{relative}/package.json")
                if not member.isfile() or member.size > 1024 * 1024:
                    raise Error(f"Invalid bundled manifest: {path}")
                metadata = json.load(archive.extractfile(member))
            if metadata.get("name") != name or metadata.get("version") != 
entry["version"]:
                raise Error(f"Bundled manifest disagrees with npm: {path}")
            bundled_count += 1
            continue
        if identity not in inventory:
            raise Error(f"Npm selected a source absent from pnpm: {identity}")
        expected = next(iter(inventory[identity]["packages"].values()))
        if entry.get("integrity") != expected["integrity"]:
            raise Error(f"Npm integrity disagrees with pnpm: {identity}")
        selected.add(identity)
    return {"source_identities": len(selected), "installation_entries": 
len(packages) - 1,
            "bundled_entries": bundled_count,
            "not_selected_by_npm": sorted(set(inventory) - selected)}


def enable_loopback():
    # Executed only after unshare creates the private namespace.
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
        flags = fcntl.ioctl(sock, 0x8913, struct.pack("16sH14x", b"lo", 0))
        value = struct.unpack("16sH14x", flags)[1]
        fcntl.ioctl(sock, 0x8914, struct.pack("16sH14x", b"lo", value | 1))


def resolve(manifest, tarballs, helper, timeout):
    with tempfile.TemporaryDirectory(prefix="git-bug-npm-lock-") as directory:
        work = Path(directory)
        (work / "package.json").write_text(json.dumps(manifest, indent=2) + 
"\n")
        # Do not inherit registry/cache settings or NODE_OPTIONS from the 
caller.
        env = {key: value for key, value in os.environ.items()
               if not key.lower().startswith("npm_config_") and key != 
"NODE_OPTIONS"}
        env.update(HOME=directory, npm_config_cache=str(work / "cache"),
                   npm_config_userconfig=str(work / "npmrc"),
                   npm_config_globalconfig=str(work / "global-npmrc"),
                   npm_config_update_notifier="false", 
NODE_OPTIONS="--max-old-space-size=1536")
        log_path = work / "registry.log"
        with log_path.open("w") as log:
            server = subprocess.Popen([*helper, str(tarballs), "--debug"],
                                      cwd=work, env=env, stdout=log, 
stderr=subprocess.STDOUT)
            try:
                deadline = time.monotonic() + timeout
                while time.monotonic() < deadline:
                    text = log_path.read_text()
                    match = re.search(r"port: (\d+)", text)
                    if match:
                        port = match[1]
                        break
                    if server.poll() is not None:
                        raise Error("Local registry exited:\n" + text[-4000:])
                    time.sleep(0.1)
                else:
                    raise Error("Local registry startup timed out")
                subprocess.run([
                    "npm", "install", "--package-lock-only", 
"--omit-lockfile-registry-resolved",
                    "--ignore-scripts", "--legacy-peer-deps", "--include=dev", 
"--no-audit",
                    "--no-fund", "--fetch-retries=0", "--maxsockets=2",
                    "--registry", f"http://localhost:{port}";,
                ], cwd=work, env=env, check=True, timeout=timeout)
                return json.loads((work / "package-lock.json").read_text())
            finally:
                server.terminate()
                try:
                    server.wait(timeout=10)
                except subprocess.TimeoutExpired:
                    server.kill()
                    server.wait()


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--webui", type=Path, required=True)
    parser.add_argument("--tarballs", type=Path, required=True)
    parser.add_argument("--output-dir", type=Path, required=True)
    parser.add_argument("--registry-helper", nargs="+", 
default=["local-npm-registry"],
                        help="helper command; defaults to local-npm-registry")
    parser.add_argument("--timeout", type=int, default=180)
    parser.add_argument("--_in-namespace", action="store_true", 
help=argparse.SUPPRESS)
    args = parser.parse_args()
    try:
        if not args._in_namespace:
            result = subprocess.run(["unshare", "-Urn", sys.executable,
                                     str(Path(__file__).resolve()), 
*sys.argv[1:], "--_in-namespace"])
            return result.returncode
        enable_loopback()
        lock = yaml.load((args.webui / "pnpm-lock.yaml").read_text(),
                         Loader=CONVERTER["UniqueKeyLoader"])
        inventory = CONVERTER["convert"](lock)
        tarballs = args.tarballs.resolve()
        CONVERTER["verify_downloads"](inventory, tarballs)
        manifest = build_manifest(json.loads((args.webui / 
"package.json").read_text()), lock)
        npm_lock = resolve(manifest, tarballs, args.registry_helper, 
args.timeout)
        report = validate_lock(npm_lock, inventory, tarballs)
        args.output_dir.mkdir(parents=True, exist_ok=True)
        for name, value in (("webui-package.json", manifest), 
("package-lock.json", npm_lock),
                            ("webui-lock-report.json", report)):
            (args.output_dir / name).write_text(json.dumps(value, indent=2) + 
"\n")
        print(f"Validated {report['source_identities']} tarball identities and "
              f"{report['bundled_entries']} bundled entries. Output: 
{args.output_dir}")
        return 0
    except (Error, OSError, ValueError, KeyError, tarfile.TarError,
            subprocess.SubprocessError, yaml.YAMLError) as exc:
        parser.exit(1, f"error: {exc}\n")


if __name__ == "__main__":
    sys.exit(main())

++++++ test-webui.py ++++++
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Smoke-test an already built git-bug binary. Requires Python 3 and Git."""
import argparse
import gzip
from html.parser import HTMLParser
import json
import os
from pathlib import Path
import re
import signal
import subprocess
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request


class Assets(HTMLParser):
    def __init__(self):
        super().__init__()
        self.urls = []

    def handle_starttag(self, tag, attrs):
        attrs = dict(attrs)
        value = attrs.get("src") if tag == "script" else attrs.get("href") if 
tag == "link" else None
        if value and value.endswith((".js", ".css")):
            self.urls.append(value)


def require(condition, message):
    if not condition:
        raise RuntimeError(message)


def smoke_test(binary):
    with tempfile.TemporaryDirectory(prefix="git-bug-webui-test-") as directory:
        work = Path(directory)
        env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
        env.update(HOME=directory, GIT_CONFIG_NOSYSTEM="1", 
GIT_CONFIG_GLOBAL="/dev/null")
        subprocess.run(["git", "init", "--quiet", str(work)], env=env, 
check=True)
        log_path = work / "server.log"
        opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
        with log_path.open("w") as log:
            server = subprocess.Popen([str(binary), "webui", "--no-open", 
"--read-only",
                                       "--bind", "127.0.0.1"], cwd=work, 
env=env,
                                      stdout=log, stderr=subprocess.STDOUT)
            try:
                base = None
                for _ in range(100):
                    text = log_path.read_text()
                    require(server.poll() is None, "Server exited:\n" + text)
                    match = re.search(r"Web UI: (http://127\.0\.0\.1:\d+)", 
text)
                    if match:
                        base = match[1]
                        try:
                            with opener.open(base, timeout=2) as response:
                                html = response.read()
                            break
                        except (urllib.error.URLError, ConnectionError):
                            pass
                    time.sleep(0.1)
                else:
                    raise RuntimeError("Server did not become ready:\n" + 
log_path.read_text())

                def get(path, compressed=False):
                    url = urllib.parse.urljoin(base, path)
                    require(urllib.parse.urlsplit(url).netloc == 
urllib.parse.urlsplit(base).netloc,
                            "Unexpected external asset: " + path)
                    request = urllib.request.Request(url, headers={
                        "Accept-Encoding": "gzip" if compressed else 
"identity"})
                    with opener.open(request, timeout=5) as response:
                        return response.read(), response.headers

                require(b"<html" in html.lower(), "Root response is not HTML")
                spa, _ = get("/_/issues")
                require(spa == html, "SPA fallback differs from root page")
                compressed, headers = get("/", True)
                require(headers.get("Content-Encoding") == "gzip", "HTML gzip 
response missing")
                require(gzip.decompress(compressed) == html, "Gzip HTML does 
not match plain response")
                parser = Assets()
                parser.feed(html.decode())
                require(any(url.endswith(".js") for url in parser.urls), "No 
embedded JavaScript")
                require(any(url.endswith(".css") for url in parser.urls), "No 
embedded stylesheet")
                fonts = set()
                for url in parser.urls:
                    plain, _ = get(url)
                    encoded, headers = get(url, True)
                    decoded = gzip.decompress(encoded) if 
headers.get("Content-Encoding") == "gzip" else encoded
                    require(plain and decoded == plain, "Asset encoding 
mismatch: " + url)
                    if url.endswith(".css"):
                        for font in 
re.findall(r'url\([\"\']?([^\s)\"\']+\.woff2)[\"\']?\)', plain.decode()):
                            fonts.add(urllib.parse.urljoin(url, font))
                require(fonts, "No embedded webfonts referenced by CSS")
                for font in fonts:
                    content, _ = get(font)
                    require(content.startswith(b"wOF2"), "Invalid webfont: " + 
font)
                request = urllib.request.Request(base + "/graphql",
                    data=json.dumps({"query": "{ __typename }"}).encode(),
                    headers={"Content-Type": "application/json"})
                with opener.open(request, timeout=5) as response:
                    result = json.load(response)
                require(result.get("data", {}).get("__typename") == "Query" and 
not result.get("errors"),
                        "GraphQL query failed: " + repr(result))
                print(f"PASS: HTML, SPA route, gzip, {len(parser.urls)} assets, 
{len(fonts)} fonts, GraphQL")
            finally:
                if server.poll() is None:
                    server.send_signal(signal.SIGINT)
                    try:
                        server.wait(timeout=10)
                    except subprocess.TimeoutExpired:
                        server.kill()
                        server.wait()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("binary", type=Path)
    args = parser.parse_args()
    smoke_test(args.binary.resolve())

++++++ test_collect_webui_licenses.py ++++++
# SPDX-License-Identifier: MIT
import contextlib
import io
import json
from pathlib import Path
import runpy
import tempfile
import unittest

COLLECT = 
runpy.run_path(str(Path(__file__).with_name("collect-webui-licenses.py")))["collect"]


class LicenseTests(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.addCleanup(self.tmp.cleanup)
        self.webui = Path(self.tmp.name) / "webui"
        self.webui.mkdir()
        self.destination = Path(self.tmp.name) / "notices"
        self.packages = {"": {"name": "test"}}

    def package(self, name, license="MIT", **fields):
        path = "node_modules/" + name
        self.packages[path] = {"version": "1.0.0", "license": license, **fields}
        directory = self.webui / path
        directory.mkdir(parents=True)
        (directory / "package.json").write_text(json.dumps({"name": name, 
"license": license}))
        (directory / "LICENSE").write_text("test notice\n")
        return directory

    def collect(self):
        (self.webui / "package-lock.json").write_text(json.dumps({"packages": 
self.packages}))
        with contextlib.redirect_stdout(io.StringIO()):
            COLLECT(self.webui, self.destination)

    def test_copy_production_notices_without_npm_manifest_filename(self):
        self.package("@scope/runtime", license="OFL-1.1")
        self.package("build-tool", dev=True)
        self.collect()
        target = self.destination / "node_modules/@scope/runtime"
        self.assertEqual((target / "LICENSE").read_text(), "test notice\n")
        self.assertTrue((target / "package-metadata.json").is_file())
        self.assertFalse((target / "package.json").exists())
        self.assertFalse((self.destination / 
"node_modules/build-tool").exists())

    def test_undeclared_license_requires_review(self):
        self.package("changed-license", license="GPL-3.0-only")
        with self.assertRaisesRegex(ValueError, "Review the spec License"):
            self.collect()

    def test_missing_notice_is_reported(self):
        package = self.package("missing-notice")
        (package / "LICENSE").unlink()
        (package / "README.md").write_text("upstream README\n")
        with contextlib.redirect_stderr(io.StringIO()) as errors:
            self.collect()
        self.assertIn("missing-notice", errors.getvalue())
        self.assertTrue((self.destination / 
"node_modules/missing-notice/README.md").is_file())

    def test_reject_path_traversal(self):
        self.packages["node_modules/../../outside"] = {"license": "MIT", 
"version": "1.0.0"}
        with self.assertRaisesRegex(ValueError, "Invalid package path"):
            self.collect()


if __name__ == "__main__":
    unittest.main()

++++++ test_pnpm_to_obs.py ++++++
# SPDX-License-Identifier: MIT
"""Offline tests: python3 -m unittest -v test_pnpm_to_obs.py."""
import base64
import copy
import hashlib
import importlib.util
import json
from pathlib import Path
import runpy
import subprocess
import sys
import tempfile
import unittest

import yaml

SCRIPT = Path(__file__).with_name("pnpm-to-obs.py")
spec = importlib.util.spec_from_file_location("pnpm_to_obs", SCRIPT)
converter = importlib.util.module_from_spec(spec)
spec.loader.exec_module(converter)
HASH = "sha512-" + base64.b64encode(bytes(64)).decode()
OTHER_HASH = "sha512-" + base64.b64encode(bytes([1]) * 64).decode()
SERVICE = Path("/usr/lib/obs/service/node_modules")


def record(**resolution):
    return {"resolution": {"integrity": HASH, **resolution}}


def fixture():
    return {
        "lockfileVersion": "9.0",
        "importers": {".": {"dependencies": {"react": {"version": "19.3.0"}}}},
        "packages": {
            "[email protected]": record(),
            "[email protected]": record(),
            "@scope/[email protected]": record(),
        },
    }


def package_entry(output, identity):
    return next(iter(output[identity]["packages"].values()))


class ConverterTests(unittest.TestCase):
    def test_names_versions_and_scopes(self):
        result = converter.convert(fixture())
        self.assertEqual(len(result), 3)
        self.assertEqual(package_entry(result, 
"@scope/[email protected]")["resolved"],
                         
"https://registry.npmjs.org/@scope/example/-/example-1.2.3.tgz";)
        self.assertEqual(package_entry(result, "[email protected]")["integrity"], 
HASH)

    def test_peer_contexts_share_source(self):
        lock = fixture()
        lock["packages"]["[email protected]([email protected]([email protected]))"] = record()
        self.assertEqual(len(converter.convert(lock)), 3)

    def test_explicit_tarball(self):
        lock = fixture()
        url = "https://example.org/releases/react.tgz?download=1";
        lock["packages"]["[email protected]"] = record(tarball=url)
        self.assertEqual(package_entry(converter.convert(lock), 
"[email protected]")["resolved"], url)

    def test_optional_and_foreign_platforms_are_retained(self):
        lock = fixture()
        lock["packages"]["@scope/[email protected]"] = {
            **record(), "optional": True, "os": ["darwin"], "cpu": ["arm64"]}
        self.assertIn("@scope/[email protected]", converter.convert(lock))

    def test_deterministic(self):
        lock = fixture()
        reverse = copy.deepcopy(lock)
        reverse["packages"] = dict(reversed(list(reverse["packages"].items())))
        self.assertEqual(json.dumps(converter.convert(lock)), 
json.dumps(converter.convert(reverse)))

    def test_reject_bad_integrities(self):
        for value in (None, "", "sha512-AA==", "sha512-!", "unknown-AAAA", HASH 
+ " " + OTHER_HASH):
            with self.subTest(value=value):
                lock = fixture()
                lock["packages"]["[email protected]"] = record(integrity=value)
                with self.assertRaises(converter.ConversionError):
                    converter.convert(lock)

    def test_reject_conflicting_peer_records(self):
        lock = fixture()
        lock["packages"]["[email protected]([email protected])"] = 
record(integrity=OTHER_HASH)
        with self.assertRaisesRegex(converter.ConversionError, "Conflicting"):
            converter.convert(lock)

    def test_reject_conflicting_url(self):
        lock = fixture()
        lock["packages"]["[email protected]"] = 
record(tarball="https://example.org/a.tgz";)
        lock["packages"]["[email protected]"] = 
record(tarball="https://example.org/a.tgz";, integrity=OTHER_HASH)
        with self.assertRaisesRegex(converter.ConversionError, "Conflicting"):
            converter.convert(lock)

    def test_reject_service_filename_collision(self):
        lock = fixture()
        lock["packages"]["[email protected]"] = 
record(tarball="https://one.example/a.tgz";)
        lock["packages"]["[email protected]"] = 
record(tarball="https://two.example/a.tgz";)
        with self.assertRaisesRegex(converter.ConversionError, "filename 
collision"):
            converter.convert(lock)

    def test_reject_unsupported_resolutions(self):
        for resolution in ({"commit": "abc"}, {"directory": "../foo"},
                           {"tarball": "http://example.org/a.tgz"},
                           {"tarball": 
"https://user:[email protected]/a.tgz"},
                           {"tarball": "https://example.org/.."},
                           {"tarball": "https://example.org/"}):
            with self.subTest(resolution=resolution):
                lock = fixture()
                lock["packages"]["[email protected]"] = record(**resolution)
                with self.assertRaises(converter.ConversionError):
                    converter.convert(lock)

    def test_reject_nonregistry_importer_references(self):
        for version in ("workspace:*", "link:../foo", "file:../foo", 
"git+https://example.org/foo";):
            with self.subTest(version=version):
                lock = fixture()
                lock["importers"]["."]["dependencies"]["react"]["version"] = 
version
                with self.assertRaises(converter.ConversionError):
                    converter.convert(lock)

    def test_reject_patches_and_versions(self):
        for change in ({"patchedDependencies": {"react": "react.patch"}},
                       {"lockfileVersion": "6.0"}, {"packages": {}}):
            with self.subTest(change=change):
                with self.assertRaises(converter.ConversionError):
                    converter.convert({**fixture(), **change})

    def test_reject_duplicate_yaml_keys(self):
        with self.assertRaisesRegex(converter.ConversionError, "Duplicate 
YAML"):
            yaml.load("packages: {}\npackages: {}\n", 
Loader=converter.UniqueKeyLoader)

    def test_verify_downloads(self):
        content = b"test tarball contents"
        integrity = "sha512-" + 
base64.b64encode(hashlib.sha512(content).digest()).decode()
        lock = {"lockfileVersion": "9.0", "packages": {"[email protected]": 
record(integrity=integrity)}}
        inventory = converter.convert(lock)
        with tempfile.TemporaryDirectory() as directory:
            path = Path(directory)
            with self.assertRaisesRegex(converter.ConversionError, 
"verification failed"):
                converter.verify_downloads(inventory, path)
            tarball = path / "example-1.0.0.tgz"
            tarball.write_bytes(content)
            self.assertEqual(converter.verify_downloads(inventory, path), 1)
            tarball.write_bytes(b"damaged")
            with self.assertRaisesRegex(converter.ConversionError, "checksum 
mismatch"):
                converter.verify_downloads(inventory, path)

    def test_cli_does_not_overwrite_output_on_invalid_input(self):
        with tempfile.TemporaryDirectory() as directory:
            source = Path(directory) / "pnpm-lock.yaml"
            target = Path(directory) / "node-sources.json"
            source.write_text("lockfileVersion: '6.0'\n")
            target.write_text("keep me")
            result = subprocess.run([sys.executable, str(SCRIPT), "--input", 
str(source),
                                     "--output", str(target)], 
capture_output=True, text=True)
            self.assertNotEqual(result.returncode, 0)
            self.assertEqual(target.read_text(), "keep me")

    def test_cli_refuses_npm_lock_filename(self):
        with tempfile.TemporaryDirectory() as directory:
            result = subprocess.run([sys.executable, str(SCRIPT), "--input", 
"unused.yaml",
                                     "--output", str(Path(directory) / 
"package-lock.json")],
                                    capture_output=True, text=True)
            self.assertNotEqual(result.returncode, 0)
            self.assertIn("NOT an npm install lock", result.stderr)

    @unittest.skipUnless(SERVICE.exists(), "obs-service-node_modules is not 
installed")
    def test_installed_service_parses_inventory_without_downloads(self):
        service = runpy.run_path(str(SERVICE), run_name="service_test")
        output = converter.convert(fixture())
        for lock in output.values():
            service["process_packagelock_file"](lock)
        modules = service["MODULE_MAP"]
        self.assertEqual(len(modules), 3)
        expected = {package_entry(output, key)["resolved"] for key in output}
        self.assertEqual({entry["url"] for entry in modules.values()}, expected)
        for filename, entry in modules.items():
            self.assertEqual(converter.service_filename(entry["url"]), filename)
            self.assertEqual(entry["algo"], "sha512")
            self.assertEqual(entry["chksum"], "00" * 64)


if __name__ == "__main__":
    unittest.main()

++++++ test_prepare_webui_lock.py ++++++
# SPDX-License-Identifier: MIT
"""Unit tests for npm preparation. No Node processes or network access."""
import base64
import copy
import io
import json
from pathlib import Path
import runpy
import tarfile
import tempfile
import unittest

HELPER = runpy.run_path(str(Path(__file__).with_name("prepare-webui-lock.py")))
HASH = "sha512-" + base64.b64encode(bytes(64)).decode()


class PreparationTests(unittest.TestCase):
    def fixture(self):
        package = {"dependencies": {"react": "^19.3.0"}, "packageManager": 
"[email protected]",
                   "pnpm": {"overrides": {"js-yaml@4": "^4.3.2"}}}
        lock = {"importers": {".": {"dependencies": {
            "react": {"specifier": "^19.3.0", "version": 
"19.3.0([email protected])"}}}},
                "overrides": {"js-yaml@4": "^4.3.2"}}
        return package, lock

    def test_pin_and_translate_without_mutating_input(self):
        package, lock = self.fixture()
        before = copy.deepcopy(package)
        result = HELPER["build_manifest"](package, lock)
        self.assertEqual(package, before)
        self.assertEqual(result["dependencies"]["react"], "19.3.0")
        self.assertEqual(result["overrides"], {"js-yaml@4": "^4.3.2"})
        self.assertNotIn("packageManager", result)
        self.assertNotIn("pnpm", result)

    def test_reject_manifest_mismatch(self):
        package, lock = self.fixture()
        package["dependencies"]["react"] = "^18.0.0"
        with self.assertRaisesRegex(ValueError, "disagree"):
            HELPER["build_manifest"](package, lock)

    def test_reject_unreviewed_overrides(self):
        package, lock = self.fixture()
        lock["overrides"] = {"parent>child": "1.0.0"}
        with self.assertRaisesRegex(ValueError, "overrides changed"):
            HELPER["build_manifest"](package, lock)

    def test_reject_workspace_importers(self):
        package, lock = self.fixture()
        lock["importers"]["another"] = {}
        with self.assertRaisesRegex(ValueError, "single root importer"):
            HELPER["build_manifest"](package, lock)

    def inventory(self):
        return {"[email protected]": {"packages": {"node_modules/example": {
            "resolved": 
"https://registry.npmjs.org/example/-/example-1.0.0.tgz";,
            "version": "1.0.0", "integrity": HASH}}}}

    def npm_lock(self):
        return {"lockfileVersion": 3, "packages": {
            "": {"name": "test"}, "node_modules/example": {"version": "1.0.0", 
"integrity": HASH}}}

    def test_source_identity_and_integrity(self):
        lock = self.npm_lock()
        result = HELPER["validate_lock"](lock, self.inventory(), Path("unused"))
        self.assertEqual(result["source_identities"], 1)
        for field, value in (("version", "2.0.0"), ("integrity", "invalid"),
                             ("resolved", "http://localhost:1234/a.tgz";), 
("link", True)):
            with self.subTest(field=field):
                changed = copy.deepcopy(lock)
                changed["packages"]["node_modules/example"][field] = value
                with self.assertRaises(ValueError):
                    HELPER["validate_lock"](changed, self.inventory(), 
Path("unused"))

    def test_bundled_dependencies_are_checked_against_parent_archive(self):
        lock = self.npm_lock()
        lock["packages"]["node_modules/example/node_modules/child"] = {
            "version": "2.0.0", "inBundle": True}
        with tempfile.TemporaryDirectory() as directory:
            content = json.dumps({"name": "child", "version": "2.0.0"}).encode()
            path = Path(directory)
            with tarfile.open(path / "example-1.0.0.tgz", "w:gz") as archive:
                entry = 
tarfile.TarInfo("package/node_modules/child/package.json")
                entry.size = len(content)
                archive.addfile(entry, io.BytesIO(content))
            result = HELPER["validate_lock"](lock, self.inventory(), path)
            self.assertEqual(result["bundled_entries"], 1)
            
lock["packages"]["node_modules/example/node_modules/child"]["version"] = "3.0.0"
            with self.assertRaisesRegex(ValueError, "Bundled manifest 
disagrees"):
                HELPER["validate_lock"](lock, self.inventory(), path)


if __name__ == "__main__":
    unittest.main()

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/git-bug/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.git-bug.new.1631729/vendor.tar.gz differ: char 21, 
line 1

++++++ webui-lock-report.json ++++++
{
  "source_identities": 1041,
  "installation_entries": 1089,
  "bundled_entries": 6,
  "not_selected_by_npm": [
    "@adobe/[email protected]",
    "@asamuzakjp/[email protected]",
    "@asamuzakjp/[email protected]",
    "@asamuzakjp/[email protected]",
    "@asamuzakjp/[email protected]",
    "@babel/[email protected]",
    "@babel/[email protected]",
    "@babel/[email protected]",
    "@bramus/[email protected]",
    "@csstools/[email protected]",
    "@csstools/[email protected]",
    "@csstools/[email protected]",
    "@csstools/[email protected]",
    "@csstools/[email protected]",
    "@csstools/[email protected]",
    "@eslint-community/[email protected]",
    "@eslint/[email protected]",
    "@eslint/[email protected]",
    "@eslint/[email protected]",
    "@eslint/[email protected]",
    "@eslint/[email protected]",
    "@exodus/[email protected]",
    "@humanfs/[email protected]",
    "@humanfs/[email protected]",
    "@humanfs/[email protected]",
    "@humanwhocodes/[email protected]",
    "@humanwhocodes/[email protected]",
    "@inquirer/[email protected]",
    "@inquirer/[email protected]",
    "@inquirer/[email protected]",
    "@inquirer/[email protected]",
    "@inquirer/[email protected]",
    "@jridgewell/[email protected]",
    "@mswjs/[email protected]",
    "@noble/[email protected]",
    "@open-draft/[email protected]",
    "@open-draft/[email protected]",
    "@open-draft/[email protected]",
    "@types/[email protected]",
    "@types/[email protected]",
    "@types/[email protected]",
    "@types/[email protected]",
    "@types/[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]"
  ]
}

++++++ webui-package.json ++++++
{
  "name": "git-bug-webui",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "codegen": "graphql-codegen --config codegen.ts",
    "lint": "oxlint",
    "lint:fix": "oxlint --fix",
    "fmt": "oxfmt",
    "fmt:check": "oxfmt --check",
    "check": "oxlint && oxfmt --check",
    "test": "vitest",
    "test:coverage": "vitest run --coverage",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  },
  "dependencies": {
    "@apollo/client": "4.3.0",
    "@base-ui/react": "1.8.0",
    "@floating-ui/react": "0.27.20",
    "@fontsource-variable/inter": "5.3.0",
    "@shikijs/langs": "4.4.3",
    "@shikijs/rehype": "4.4.3",
    "@shikijs/themes": "4.4.3",
    "@tanstack/react-router": "1.170.36",
    "class-variance-authority": "0.7.1",
    "clsx": "2.1.1",
    "date-fns": "4.4.0",
    "graphql": "17.0.2",
    "hast-util-to-jsx-runtime": "2.3.6",
    "lucide-react": "1.45.0",
    "react": "19.3.0",
    "react-dom": "19.3.0",
    "react-markdown": "10.1.0",
    "rehype-autolink-headings": "7.1.0",
    "rehype-external-links": "3.0.0",
    "rehype-raw": "7.0.0",
    "rehype-sanitize": "6.0.0",
    "rehype-slug": "6.0.0",
    "remark-emoji": "5.0.2",
    "remark-gfm": "4.0.1",
    "rxjs": "7.8.2",
    "shadcn": "4.21.0",
    "shiki": "4.4.3",
    "tailwind-merge": "3.7.0",
    "tw-animate-css": "1.4.0",
    "unified": "11.0.5",
    "valibot": "1.5.0"
  },
  "devDependencies": {
    "@graphql-codegen/cli": "7.4.1",
    "@graphql-codegen/client-preset": "6.2.0",
    "@graphql-typed-document-node/core": "3.2.0",
    "@storybook/addon-a11y": "10.6.0",
    "@storybook/addon-vitest": "10.6.0",
    "@storybook/react": "10.6.0",
    "@storybook/react-vite": "10.6.0",
    "@tailwindcss/typography": "0.5.20",
    "@tailwindcss/vite": "4.3.3",
    "@tanstack/eslint-plugin-router": "1.162.0",
    "@tanstack/router-plugin": "1.168.38",
    "@testing-library/dom": "10.4.1",
    "@testing-library/jest-dom": "7.0.1",
    "@testing-library/react": "16.3.3",
    "@tsconfig/strictest": "2.0.8",
    "@tsconfig/vite-react": "8.1.1",
    "@types/node": "22.20.2",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0",
    "@vitejs/plugin-react": "6.1.1",
    "@vitest/browser-playwright": "4.1.11",
    "@vitest/coverage-v8": "4.1.11",
    "@vitest/ui": "4.1.11",
    "eslint-plugin-storybook": "10.6.0",
    "happy-dom": "20.14.5",
    "oxfmt": "0.67.0",
    "oxlint": "1.82.0",
    "oxlint-tsgolint": "7.0.2001",
    "playwright": "1.63.0",
    "storybook": "10.6.0",
    "tailwindcss": "4.3.3",
    "typescript": "7.0.2",
    "vite": "8.3.0",
    "vite-plugin-compression": "0.5.1",
    "vite-plugin-svgr": "5.2.0",
    "vitest": "4.1.11"
  },
  "overrides": {
    "js-yaml@4": "^4.3.2"
  }
}

Reply via email to