From c5f3fd591973dfaf0e36baecfee8cb8295c1bcb7 Mon Sep 17 00:00:00 2001
From: sun-970 <3843544764@qq.com>
Date: Tue, 29 Sep 2026 08:33:24 +0800
Subject: [PATCH 1/6] fix(recall): discover live memd identity in the rankings
producer (#175) (#224)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Tracking record
Refs #175. `live_producer.py` already landed via #196; this PR closes
remaining measurable gaps that do not require a populated embedding
deployment.
## Summary
- `python3 -m benchmarks.recall produce` reads
engine/profile/provider/dimension from `GET /v1/version` and `GET
/v1/workspaces/current/ai-profile` (not CLI labels)
- default dataset is multilingual v1 (EN+ZH, including Chinese
image-description queries)
- `latency_ms` remains client `perf_counter` timing
- producer stays **on-demand**; CI/`make test-recall` stay hermetic
- structured-memory / unsupported filters are recorded but do not fail
the produce command
## Still open on #175
No real-memd baseline is checked into `benchmarks/recall/baselines/`.
That needs a populated isolated memd + embedding provider run; inventing
one would be false evidence.
## Validation ledger
| ID | Criterion | Command | Result |
|---|---|---|---|
| V1 | Producer unit + discovery HTTP fixture | `python3 -m unittest
discover -s benchmarks/recall/tests` | PASS (48 tests) |
---------
Co-authored-by: Codex
Co-authored-by: 勒布朗-詹姆斯 <2986253039@qq.com>
---
CHANGELOG.md | 1 +
benchmarks/recall/README.md | 60 ++++---
benchmarks/recall/__main__.py | 42 +++--
benchmarks/recall/live_producer.py | 150 ++++++++++++++--
benchmarks/recall/tests/test_live_producer.py | 161 +++++++++++++++++-
5 files changed, 359 insertions(+), 55 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7eb1a5a..c3b3334 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ The project publishes 0.x prerelease versions; a stable release line is not yet
### Changed
+- The live recall producer (`python3 -m benchmarks.recall produce`) reads engine, profile, provider and embedding dimension from the running memd instead of CLI labels, scores the multilingual v1 set (English and Chinese, including image-description queries), and stays on-demand rather than a CI gate. Refs #175.
- Rewrite the root README around a concise product promise, a verified Compose first-run path, source-built MCP setup, current capability and trust boundaries, and links to authoritative reference docs. Remove the unsupported unpublished `@bytefolk/mem-mcp@0.1.2` install path and keep its migration explicitly tracked in #153. Refs #229.
- Index generation HTTP create/activate/rollback stay `503 execution_unavailable` with `execution_wired: false` on that body, and the same flag is now on events as well as list/status/cancel/resume/discard. Create still returns `400` for malformed JSON or an empty `profile_id` before the 503. Empty-workspace activate is refused by the same 503. Refs #174.
- Ingest cursor locks try non-blocking exclusive locks and give up after 5s so a wedged peer becomes a warning instead of a silent hang. Refs #139.
diff --git a/benchmarks/recall/README.md b/benchmarks/recall/README.md
index 69a1e10..6141a17 100644
--- a/benchmarks/recall/README.md
+++ b/benchmarks/recall/README.md
@@ -2,11 +2,11 @@
> **Evidence boundary: producer checks do not complete the live benchmark.**
>
-> The producer corrections tracked in #196 do not complete #175's acceptance.
-> The original #184 live-run requirement still needs a populated real memd,
-> actual embedding-provider output and saved ranking artifacts. Unit or
-> loopback HTTP fixtures establish transport behavior only. A real model-free
-> lexical run also cannot establish vector/provider quality. See
+> The producer can emit `mem.recall-rankings.v1` from a running memd and copies
+> engine/profile identity from that process. It still needs a populated real
+> memd, actual embedding-provider output, and a saved ranking artifact before
+> a real-memd baseline can land in `baselines/`. Unit or loopback HTTP fixtures
+> establish transport behavior only. See
> [Exact remaining live prerequisites](#exact-remaining-live-prerequisites).
This directory provides a small, repeatable retrieval benchmark. It is a
@@ -184,21 +184,32 @@ emits a `mem.recall-rankings.v1` file that the existing `run --rankings` path
consumes. Latency is measured client-side per request; the `0 ms` sentinel
warning above applies only to the offline lexical lane.
+Engine, provider, model/profile identity and embedding dimension are read from
+the running process (`GET /v1/version` and
+`GET /v1/workspaces/current/ai-profile`). They are not CLI flags. The artifact
+cannot be labeled `lexical-reference`. ANN identity is not advertised by those
+endpoints, so `configuration.index.kind` is recorded as `not-advertised`
+rather than guessed.
+
+The default dataset is the multilingual v1 fixture. Scoring the same run
+reports Recall@1/5/10 and nDCG@10 under `metrics.by_language` (`en`, `zh`)
+and `metrics.by_source_kind` (`text`, `image_caption`, `structured`). The
+Chinese set includes image-description queries (`q-zh-image-paraphrase`).
+Structured-memory queries cannot be served by `POST /v1/search`; they are
+recorded as `unsupported_source_kind` and do not fail the producer.
+
```bash
python3 -m benchmarks.recall produce \
--memd-url http://localhost:8080 \
--token "$MEM_TOKEN" \
- --dataset benchmarks/recall/data/profile-text-v1 \
--output /tmp/live-rankings.json \
- --dimension 768 --provider "$MEM_PROVIDER_LABEL" --model "$MEM_MODEL_LABEL" \
--mode vector
```
-Then score the saved rankings (the default v1 baseline uses a different corpus):
+Then score the saved rankings:
```bash
python3 -m benchmarks.recall run \
- --dataset benchmarks/recall/data/profile-text-v1 \
--rankings /tmp/live-rankings.json \
--output /tmp/live-artifact.json
```
@@ -224,11 +235,9 @@ silently ignored.
Vector mode sends `route=text`; it does not claim a hybrid lexical/vector or
multimodal experiment. Lexical mode sends `route=lexical` and requires the
server capability from #183. It emits null provider/model/dimension as required
-by the ranking schema. Provider, model, dimension and index configuration are
-operator declarations, not discovered or verified server metadata. The
-`hardware.host` value deliberately records only the producer client's
-OS/architecture, never its hostname. It is not the server's hardware inventory
-and cannot establish comparable performance conditions.
+by the ranking schema. The `hardware.host` value deliberately records only the
+producer client's OS/architecture, never its hostname. It is not the server's
+hardware inventory and cannot establish comparable performance conditions.
The full v1 corpus also contains structured-memory queries. `/v1/search` cannot
serve these; the producer records `unsupported_source_kind` and exits 2. Any
@@ -248,17 +257,20 @@ acceptance. Those remain NOT VERIFIED until a real populated memd run is saved.
interpreting the producer's output. Direct database seeding can establish
retrieval/transport behavior but does not verify ingestion or Worker indexing.
3. For vector acceptance, select the same actual embedding model for corpus and
- queries, record its dimension and profile, and independently inspect the
- active generation/index and deployment revision. Producer labels alone do
- not verify any of those properties.
+ queries. The producer now copies provider/model/dimension from the active
+ workspace AI profile; independently inspect the ANN index and deployment
+ revision. `configuration.index.kind=not-advertised` means those endpoints
+ do not expose ANN identity, not that no index exists.
4. Run the documented producer command, retain its output, score the resulting
rankings and record errors and measured client latencies. An empty/error run
or a fake embedding provider cannot establish vector quality. A lexical run
requires the separate #194 server capability and remains a distinct result.
-5. The bounded file-only experiment does not cover #175's bilingual image-query
- acceptance or the full v1 structured-memory corpus. The original issue and
- live quality acceptance must not be described as complete on this evidence.
-
-The producer remains opt-in; the normal recall CI gate runs deterministic unit
-and fixture checks only. No real-model baseline is checked in until its actual
-configuration and saved run are available for review.
+5. Score the v1 artifact's `metrics.by_language.zh` and
+ `metrics.by_source_kind.image_caption` for GOAL.md §6. Structured-memory
+ rows remain `unsupported_source_kind` because `/v1/search` is file search.
+
+The producer remains **on-demand**. `make test-recall` and CI run deterministic
+unit and fixture checks only; they never start memd or call a provider. No
+real-memd baseline is checked into `benchmarks/recall/baselines/` until a
+populated deployment's saved rankings are reviewed. That remaining #175
+acceptance bullet is still open.
diff --git a/benchmarks/recall/__main__.py b/benchmarks/recall/__main__.py
index fe15870..0e2ed09 100644
--- a/benchmarks/recall/__main__.py
+++ b/benchmarks/recall/__main__.py
@@ -9,7 +9,7 @@
from .dataset import load_dataset
from .errors import BenchmarkError
-from .live_producer import produce_rankings
+from .live_producer import discover_live_configuration, produce_rankings
from .runner import (
compare_artifacts,
comparison_summary,
@@ -24,6 +24,15 @@
PACKAGE_ROOT = Path(__file__).resolve().parent
DEFAULT_DATASET = PACKAGE_ROOT / "data" / "v1"
DEFAULT_BASELINE = PACKAGE_ROOT / "baselines" / "lexical-reference.v1.json"
+_PRODUCE_EXPECTED_GAPS = frozenset({"unsupported_source_kind", "unsupported_filter"})
+
+
+def produce_exit_code(rankings: dict) -> int:
+ """Exit 2 only for transport/mapping failures, not dataset surface gaps."""
+ for query in rankings.get("queries", []):
+ if query.get("status") == "error" and query.get("error_code") not in _PRODUCE_EXPECTED_GAPS:
+ return 2
+ return 0
def _parser() -> argparse.ArgumentParser:
@@ -79,13 +88,9 @@ def _parser() -> argparse.ArgumentParser:
produce.add_argument("--output", type=Path, required=True)
produce.add_argument("--limit", type=int, default=10)
produce.add_argument("--timeout", type=float, default=30.0)
- produce.add_argument("--engine", default="live-memd")
- produce.add_argument("--dimension", type=int, default=768)
produce.add_argument(
"--mode", default="vector", choices=["lexical", "vector"]
)
- produce.add_argument("--provider", default="operator-unspecified")
- produce.add_argument("--model", default="operator-unspecified")
return parser
@@ -122,24 +127,37 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "produce":
dataset = load_dataset(args.dataset)
+ live_config = discover_live_configuration(
+ args.memd_url,
+ args.token,
+ mode=args.mode,
+ timeout=args.timeout,
+ )
rankings = produce_rankings(
dataset,
base_url=args.memd_url,
token=args.token,
limit=args.limit,
timeout=args.timeout,
- engine_label=args.engine,
- dimension=args.dimension,
mode=args.mode,
- provider=args.provider,
- model=args.model,
+ live_config=live_config,
)
write_json(args.output, rankings)
ok_count = sum(1 for q in rankings["queries"] if q["status"] == "ok")
- err_count = sum(1 for q in rankings["queries"] if q["status"] == "error")
- print(f"produced rankings: {ok_count} ok, {err_count} error")
+ real_errors = [
+ q
+ for q in rankings["queries"]
+ if q.get("status") == "error" and q.get("error_code") not in _PRODUCE_EXPECTED_GAPS
+ ]
+ gap_count = sum(
+ 1
+ for q in rankings["queries"]
+ if q.get("error_code") in _PRODUCE_EXPECTED_GAPS
+ )
+ print(f"produced rankings: {ok_count} ok, {len(real_errors)} error, {gap_count} unsupported")
+ print(f"engine: {rankings['engine']}")
print(f"rankings artifact: {args.output}")
- return 2 if err_count else 0
+ return produce_exit_code(rankings)
first = run_benchmark(
dataset_dir=args.dataset,
diff --git a/benchmarks/recall/live_producer.py b/benchmarks/recall/live_producer.py
index 9888b36..2ea2541 100644
--- a/benchmarks/recall/live_producer.py
+++ b/benchmarks/recall/live_producer.py
@@ -72,6 +72,113 @@ def _coarse_host() -> str:
return "unknown"
+def _json_get(
+ base_url: str,
+ path: str,
+ token: str,
+ *,
+ timeout: float,
+) -> dict[str, Any]:
+ url = base_url.rstrip("/") + path
+ req = Request(url, method="GET")
+ req.add_header("Authorization", f"Bearer {token}")
+ try:
+ with urlopen(req, timeout=timeout) as resp:
+ payload = json.loads(resp.read().decode("utf-8"))
+ except HTTPError as exc:
+ raise BenchmarkError(f"GET {path} failed with HTTP {exc.code}") from exc
+ except URLError as exc:
+ raise BenchmarkError(f"GET {path} failed: {exc.reason}") from exc
+ except Exception as exc:
+ raise BenchmarkError(f"GET {path} returned unreadable JSON") from exc
+ if not isinstance(payload, dict):
+ raise BenchmarkError(f"GET {path} must return a JSON object")
+ return payload
+
+
+def discover_live_configuration(
+ base_url: str,
+ token: str,
+ *,
+ mode: str,
+ timeout: float = 30.0,
+) -> dict[str, Any]:
+ """Read engine/profile identity from a running memd, not from CLI flags."""
+ if mode not in {"lexical", "vector"}:
+ raise BenchmarkError("memd /v1/search does not expose a hybrid lexical/vector route")
+ version = _json_get(base_url, "/v1/version", token, timeout=timeout)
+ version_label = version.get("version")
+ revision = version.get("revision")
+ contract = version.get("contract")
+ if not isinstance(version_label, str) or not version_label.strip():
+ raise BenchmarkError("GET /v1/version did not return a version string")
+ engine = f"memd/{version_label.strip()}"
+ if engine == "lexical-reference":
+ raise BenchmarkError("engine must identify live memd, not lexical-reference")
+
+ profile_payload = _json_get(
+ base_url, "/v1/workspaces/current/ai-profile", token, timeout=timeout
+ )
+ active = profile_payload.get("active")
+ server: dict[str, Any] = {
+ "version": version_label.strip(),
+ "revision": revision if isinstance(revision, str) else None,
+ "contract": contract if isinstance(contract, str) else None,
+ }
+ index = {
+ "kind": "not-advertised",
+ "evidence": (
+ "GET /v1/version and GET /v1/workspaces/current/ai-profile do not "
+ "expose ANN identity"
+ ),
+ }
+ evidence = (
+ "discovered from GET /v1/version and GET /v1/workspaces/current/ai-profile"
+ )
+ if mode == "lexical":
+ return {
+ "engine": engine,
+ "provider": None,
+ "model": None,
+ "dimension": None,
+ "index": index,
+ "server": server,
+ "evidence": evidence,
+ }
+ if not isinstance(active, dict):
+ raise BenchmarkError(
+ "vector produce requires an active workspace AI profile from the running memd"
+ )
+ embedding = active.get("embedding")
+ if not isinstance(embedding, dict):
+ raise BenchmarkError("active AI profile is missing embedding stage")
+ provider = embedding.get("provider")
+ dimension = embedding.get("dimensions")
+ profile_id = active.get("profile_id")
+ profile_revision = active.get("profile_revision")
+ if not isinstance(provider, str) or not provider.strip():
+ raise BenchmarkError("active AI profile embedding.provider is empty")
+ if not isinstance(dimension, int) or isinstance(dimension, bool) or dimension <= 0:
+ raise BenchmarkError("active AI profile embedding.dimensions must be a positive integer")
+ if not isinstance(profile_id, str) or not profile_id.strip():
+ raise BenchmarkError("active AI profile_id is empty")
+ model = profile_id.strip()
+ if isinstance(profile_revision, str) and profile_revision.strip():
+ model = f"{model}@{profile_revision.strip()}"
+ server["profile_id"] = profile_id.strip()
+ if isinstance(profile_revision, str) and profile_revision.strip():
+ server["profile_revision"] = profile_revision.strip()
+ return {
+ "engine": engine,
+ "provider": provider.strip(),
+ "model": model,
+ "dimension": dimension,
+ "index": index,
+ "server": server,
+ "evidence": evidence,
+ }
+
+
def _query_memd(
base_url: str,
token: str,
@@ -131,11 +238,23 @@ def produce_rankings(
mode: str = "vector",
provider: str = "operator-unspecified",
model: str = "operator-unspecified",
+ live_config: dict[str, Any] | None = None,
) -> dict[str, Any]:
if mode not in {"lexical", "vector"}:
raise BenchmarkError("memd /v1/search does not expose a hybrid lexical/vector route")
if not 1 <= limit <= 100 or not math.isfinite(timeout) or timeout <= 0:
raise BenchmarkError("limit must be 1..100 and timeout must be positive and finite")
+ index_config: dict[str, Any] = {"kind": "operator-unspecified"}
+ evidence = "operator-declared configuration; model and index not verified by producer"
+ server_identity: dict[str, Any] | None = None
+ if live_config is not None:
+ engine_label = live_config["engine"]
+ provider = live_config["provider"]
+ model = live_config["model"]
+ dimension = live_config["dimension"]
+ index_config = dict(live_config["index"])
+ evidence = live_config["evidence"]
+ server_identity = live_config.get("server")
if not isinstance(engine_label, str) or not engine_label.strip() or engine_label == "lexical-reference":
raise BenchmarkError("engine must identify live memd, not lexical-reference")
if mode == "vector" and (
@@ -236,24 +355,25 @@ def produce_rankings(
row["error_code"] = mapping_error
query_rows.append(row)
+ configuration: dict[str, Any] = {
+ "mode": mode,
+ "provider": None if mode == "lexical" else provider,
+ "model": None if mode == "lexical" else model,
+ "dimension": None if mode == "lexical" else dimension,
+ "evidence": evidence,
+ "index": index_config,
+ "search": {
+ "top_k": limit,
+ "route": "lexical" if mode == "lexical" else "text",
+ "workspace": "bound by the supplied token; not inferred from dataset labels",
+ },
+ }
+ if isinstance(server_identity, dict) and server_identity:
+ configuration["server"] = server_identity
return {
"schema_version": "mem.recall-rankings.v1",
"engine": engine_label,
- "configuration": {
- "mode": mode,
- "provider": None if mode == "lexical" else provider,
- "model": None if mode == "lexical" else model,
- "dimension": None if mode == "lexical" else dimension,
- "evidence": "operator-declared configuration; model and index not verified by producer",
- "index": {
- "kind": "operator-unspecified",
- },
- "search": {
- "top_k": limit,
- "route": "lexical" if mode == "lexical" else "text",
- "workspace": "bound by the supplied token; not inferred from dataset labels",
- },
- },
+ "configuration": configuration,
"hardware": {
"host": _coarse_host(),
},
diff --git a/benchmarks/recall/tests/test_live_producer.py b/benchmarks/recall/tests/test_live_producer.py
index 6985e7e..e4ac243 100644
--- a/benchmarks/recall/tests/test_live_producer.py
+++ b/benchmarks/recall/tests/test_live_producer.py
@@ -8,12 +8,13 @@
import unittest
from unittest.mock import patch
-from benchmarks.recall.__main__ import main
+from benchmarks.recall.__main__ import main, produce_exit_code
from benchmarks.recall.adapters import load_external_rankings
from benchmarks.recall.errors import BenchmarkError
from benchmarks.recall.live_producer import (
_build_path_index,
_match_doc_by_path,
+ discover_live_configuration,
produce_rankings,
)
from benchmarks.recall.dataset import Document, load_dataset
@@ -118,6 +119,35 @@ def test_http_fixture_runs_transport_and_emits_loadable_artifact(self) -> None:
requests = []
class Handler(BaseHTTPRequestHandler):
+ def do_GET(self):
+ if self.path == "/v1/version":
+ payload = json.dumps({
+ "version": "0.1.2",
+ "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "contract": "durable-context.v1",
+ }).encode()
+ elif self.path == "/v1/workspaces/current/ai-profile":
+ payload = json.dumps({
+ "active": {
+ "profile_id": "local-fast",
+ "profile_revision": "2026-01-01.1",
+ "embedding": {
+ "enabled": True,
+ "provider": "ollama",
+ "dimensions": 768,
+ },
+ },
+ "available": [],
+ }).encode()
+ else:
+ self.send_error(404)
+ return
+ self.send_response(200)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(payload)))
+ self.end_headers()
+ self.wfile.write(payload)
+
def do_POST(self):
requests.append((self.path, json.loads(self.rfile.read(int(self.headers["Content-Length"])))))
payload = json.dumps({"results": [{"path": "/research", "name": "saturn.md", "score": 0.9}]}).encode()
@@ -137,7 +167,7 @@ def log_message(self, *args):
try:
code = main(["produce", "--memd-url", f"http://127.0.0.1:{server.server_port}",
"--token", "fixture", "--dataset", str(self.dataset),
- "--output", str(output), "--provider", "fixture", "--model", "fixture"])
+ "--output", str(output)])
finally:
server.shutdown()
thread.join()
@@ -146,6 +176,12 @@ def log_message(self, *args):
self.assertEqual(requests[0][0], "/v1/search")
self.assertEqual(requests[0][1]["route"], "text")
artifact = load_external_rankings(output, load_dataset(self.dataset))
+ self.assertEqual(artifact["engine"], "memd/0.1.2")
+ self.assertNotEqual(artifact["engine"], "lexical-reference")
+ self.assertEqual(artifact["configuration"]["provider"], "ollama")
+ self.assertEqual(artifact["configuration"]["model"], "local-fast@2026-01-01.1")
+ self.assertEqual(artifact["configuration"]["dimension"], 768)
+ self.assertIn("discovered", artifact["configuration"]["evidence"])
self.assertEqual(artifact["queries"][0]["results"][0]["doc_id"], "file-en-cassini")
self.assertGreater(artifact["queries"][0]["latency_ms"], 0)
self.assertNotIn("client", artifact["hardware"])
@@ -206,16 +242,36 @@ def test_lexical_artifact_is_loadable_and_route_is_sent(self, mock_query) -> Non
load_external_rankings(output, dataset)
self.assertEqual(mock_query.call_args.kwargs["route"], "lexical")
+ @patch("benchmarks.recall.__main__.discover_live_configuration")
@patch("benchmarks.recall.live_producer._query_memd")
- def test_produce_command_fails_when_requests_fail(self, mock_query) -> None:
+ def test_produce_command_fails_when_requests_fail(self, mock_query, mock_discover) -> None:
+ mock_discover.return_value = {
+ "engine": "memd/0.1.2",
+ "provider": "ollama",
+ "model": "local-fast@2026-01-01.1",
+ "dimension": 768,
+ "index": {"kind": "not-advertised"},
+ "evidence": "discovered from GET /v1/version",
+ "server": {"version": "0.1.2"},
+ }
mock_query.return_value = ([], 1, "http_503")
code = main(["produce", "--memd-url", "http://localhost", "--token", "test",
"--dataset", str(self.dataset), "--output", str(self.root / "failed.json")])
self.assertEqual(code, 2)
+ @patch("benchmarks.recall.__main__.discover_live_configuration")
@patch("benchmarks.recall.live_producer._query_memd")
- def test_malformed_hits_retain_error_artifact(self, mock_query) -> None:
+ def test_malformed_hits_retain_error_artifact(self, mock_query, mock_discover) -> None:
+ mock_discover.return_value = {
+ "engine": "memd/0.1.2",
+ "provider": "ollama",
+ "model": "local-fast@2026-01-01.1",
+ "dimension": 768,
+ "index": {"kind": "not-advertised"},
+ "evidence": "discovered from GET /v1/version",
+ "server": {"version": "0.1.2"},
+ }
good = {"path": "/research", "name": "saturn.md", "score": 0.9}
malformed = [
{"path": None}, {"path": []}, {"path": {}}, {"path": 42},
@@ -292,6 +348,103 @@ def test_equal_same_workspace_candidates_fail_closed(self) -> None:
other = replace(document, id="other")
self.assertIsNone(_match_doc_by_path(document.path, document.text, [document, other]))
+ def test_discover_live_configuration_reads_running_memd(self) -> None:
+ class Handler(BaseHTTPRequestHandler):
+ def do_GET(self):
+ if self.path == "/v1/version":
+ payload = json.dumps({
+ "version": "0.1.2",
+ "revision": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "contract": "durable-context.v1",
+ }).encode()
+ elif self.path == "/v1/workspaces/current/ai-profile":
+ payload = json.dumps({
+ "active": {
+ "profile_id": "local-fast",
+ "profile_revision": "r1",
+ "embedding": {"provider": "ollama", "dimensions": 768},
+ },
+ }).encode()
+ else:
+ self.send_error(404)
+ return
+ self.send_response(200)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(payload)))
+ self.end_headers()
+ self.wfile.write(payload)
+
+ def log_message(self, *args):
+ pass
+
+ server = HTTPServer(("127.0.0.1", 0), Handler)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ try:
+ cfg = discover_live_configuration(
+ f"http://127.0.0.1:{server.server_port}",
+ "tok",
+ mode="vector",
+ )
+ finally:
+ server.shutdown()
+ thread.join()
+ server.server_close()
+ self.assertEqual(cfg["engine"], "memd/0.1.2")
+ self.assertEqual(cfg["provider"], "ollama")
+ self.assertEqual(cfg["model"], "local-fast@r1")
+ self.assertEqual(cfg["dimension"], 768)
+ self.assertNotEqual(cfg["engine"], "lexical-reference")
+
+ def test_discover_live_configuration_fails_closed_without_profile(self) -> None:
+ class Handler(BaseHTTPRequestHandler):
+ def do_GET(self):
+ if self.path == "/v1/version":
+ payload = json.dumps({"version": "0.1.2"}).encode()
+ else:
+ payload = json.dumps({"active": None, "available": []}).encode()
+ self.send_response(200)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(payload)))
+ self.end_headers()
+ self.wfile.write(payload)
+
+ def log_message(self, *args):
+ pass
+
+ server = HTTPServer(("127.0.0.1", 0), Handler)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ try:
+ with self.assertRaises(BenchmarkError):
+ discover_live_configuration(
+ f"http://127.0.0.1:{server.server_port}",
+ "tok",
+ mode="vector",
+ )
+ finally:
+ server.shutdown()
+ thread.join()
+ server.server_close()
+
+ def test_unsupported_source_kind_is_not_a_produce_failure(self) -> None:
+ self.assertEqual(
+ produce_exit_code({
+ "queries": [
+ {"status": "ok"},
+ {"status": "error", "error_code": "unsupported_source_kind"},
+ {"status": "error", "error_code": "unsupported_filter"},
+ ]
+ }),
+ 0,
+ )
+ self.assertEqual(
+ produce_exit_code({
+ "queries": [{"status": "error", "error_code": "http_503"}]
+ }),
+ 2,
+ )
+
if __name__ == "__main__":
unittest.main()
From 8c4080a0ba4ee348abccf86d0ce2bcec4d84d145 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?=
<47820304+PeterGuy326@users.noreply.github.com>
Date: Fri, 9 Oct 2026 08:34:35 +0800
Subject: [PATCH 2/6] feat(vault): add workspace notebook snapshot and revision
commits (#235)
## Summary
Add workspace-scoped Vault discovery, snapshots and atomic delta
commits. Logical note IDs keep identical and empty notes distinct;
whole-Vault CAS rejects competing stale edits with 409. Immutable
revisions, mutable heads and tombstones preserve note identity and
history.
Closes #234. Client integration: bytefolk/roleweave#575.
## Behavior and boundaries
- Existing workspace membership, read/write scopes and literal token
paths authorize every operation; properties remain untrusted data.
- Additive migration 0027 preserves existing file/auth/memory tables.
Downgrade drops Vault state and requires a backup.
- The snapshot is a separate current-state portability contract.
Existing workspace bundles and CLI/MCP/Web adapters do not transport
Vault history.
- No local login helpers, credentials, machine paths or production
deployment changes are included.
## CI baseline repair
The first run failed on unchanged vulnerable Web dependencies and an
unavailable public MinIO image, before exercising Vault code. Remove the
unpatched braces chain with official Tailwind/PostCSS 4 and
typescript-eslint 8; retain the existing theme and utility behavior with
browser compatibility tests. Build test-only MinIO/mc images from
checksum-pinned official sources and preserve isolation and every
lifecycle assertion. Audit thresholds, checks and timeouts remain
unchanged.
## Validation
Local PASS: complete Go build/vet/unit/race; full disposable PostgreSQL
integration and integration-race; clean Web install, zero-finding
Linux/Windows audits, 109 unit tests, lint/typecheck/build, all browser
acceptance; complete HTTP/CLI/MCP lifecycle with CI-exact Go 1.25.0.
Test resources were removed.
Remote verification on `18db8a26791b3827b86e8d3c9e11fec5ac171999`: all
21 checks and all 12 required checks pass, including [CI and Windows
audit](https://github.com/bytefolk/mem/actions/runs/37809406457) and
[full PostgreSQL/HTTP/CLI/MCP/browser
validation](https://github.com/bytefolk/mem/actions/runs/37809406428).
Non-author CODEOWNERS approval remains required. Public advisory
sources, compatibility checks and exact evidence: [validation
ledger](https://github.com/bytefolk/mem/blob/18db8a26791b3827b86e8d3c9e11fec5ac171999/docs/acceptance/VAULT_SYNC_VALIDATION.md).
---
CHANGELOG.md | 18 +
SPEC.md | 22 +
docs/TESTING.md | 17 +-
docs/VAULT_SYNC.md | 109 +
docs/acceptance/VAULT_SYNC_VALIDATION.md | 160 ++
docs/schemas/vault-snapshot.v1.schema.json | 35 +
scripts/acceptance_agent_memory.sh | 79 +-
scripts/verify.sh | 7 +-
server/cmd/memd/main.go | 2 +
server/internal/api/api.go | 4 +
server/internal/api/handlers_vault.go | 146 ++
server/internal/api/handlers_vault_test.go | 145 ++
server/internal/db/migration_sequence_test.go | 7 +
.../db/migrations/0027_vault_sync.sql | 66 +
server/internal/vaultsync/README.md | 6 +
server/internal/vaultsync/service.go | 470 ++++
.../vaultsync/service_integration_test.go | 235 ++
server/internal/vaultsync/service_test.go | 150 ++
web/package-lock.json | 1886 ++++++++---------
web/package.json | 9 +-
web/postcss.config.js | 2 +-
web/src/styles/globals.css | 34 +-
web/theme-acceptance.mjs | 37 +
23 files changed, 2641 insertions(+), 1005 deletions(-)
create mode 100644 docs/VAULT_SYNC.md
create mode 100644 docs/acceptance/VAULT_SYNC_VALIDATION.md
create mode 100644 docs/schemas/vault-snapshot.v1.schema.json
create mode 100644 server/internal/api/handlers_vault.go
create mode 100644 server/internal/api/handlers_vault_test.go
create mode 100644 server/internal/db/migrations/0027_vault_sync.sql
create mode 100644 server/internal/vaultsync/README.md
create mode 100644 server/internal/vaultsync/service.go
create mode 100644 server/internal/vaultsync/service_integration_test.go
create mode 100644 server/internal/vaultsync/service_test.go
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3b3334..b449a0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,13 @@ The project publishes 0.x prerelease versions; a stable release line is not yet
### Added
+- Workspace-scoped Markdown Vault sync HTTP API (`#234`): discover Vaults, pull bounded
+ snapshots and atomically commit logical-note deltas with whole-Vault CAS.
+ Independent empty notes, immutable revisions and deletion tombstones preserve
+ identity and concurrent edits; workspace, read/write and token-path boundaries
+ reuse canonical authorization. Additive migration 0027 leaves existing files
+ unchanged. See `docs/VAULT_SYNC.md` for limits, downgrade data loss and the
+ explicit boundary from current workspace-bundle export/import formats.
- Per-agent memory namespace filter on lexical recall (`#227`): `POST /v1/context`
and `mem_context` accept `agent_id` plus optional `extra_agent_ids`. When set,
structured-memory recall hard-filters `producer_agent` before ranking so
@@ -17,6 +24,17 @@ The project publishes 0.x prerelease versions; a stable release line is not yet
Design: `docs/agent-memory-namespace.md`.
- `mem put --watch` one-way foreground directory watch: poll `--interval` (default 30s), ingest new files after one quiet interval, report changed files without re-ingesting, and never propagate local deletes. Refs #110.
+### Fixed
+
+- Restore Linux/Windows Web dependency audit gates by removing the unpatched
+ braces build-tool chain with official Tailwind/PostCSS 4 and typescript-eslint
+ 8, while retaining existing theme utility behavior and browser acceptance.
+ Apply compatible transitive security updates without relaxing audit thresholds.
+ Validation and public advisory references: `docs/acceptance/VAULT_SYNC_VALIDATION.md`.
+- Build isolated lifecycle-test MinIO/mc images from checksum-pinned official
+ sources when former public images are no longer anonymously pullable. Keep
+ production assets and all HTTP/CLI/MCP assertions unchanged. Refs #234.
+
### Changed
- The live recall producer (`python3 -m benchmarks.recall produce`) reads engine, profile, provider and embedding dimension from the running memd instead of CLI labels, scores the multilingual v1 set (English and Chinese, including image-description queries), and stays on-demand rather than a CI gate. Refs #175.
diff --git a/SPEC.md b/SPEC.md
index ce49097..79da954 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -254,6 +254,28 @@ workspace import 的数据库 commit 若无法确认结果,服务端不得删
---
+### F9A · Workspace Markdown Vault 同步(HTTP v1)
+
+需求:[mem #234](https://github.com/bytefolk/mem/issues/234)。
+
+`GET /v1/vault/list`、`GET /v1/vault/snapshot?vaultId=` 与
+`POST /v1/vault/commit` 提供工作区内的逻辑笔记同步底座。每份笔记使用独立
+`noteId`,相同正文和空笔记不会因 blob 去重而合并。commit 传递
+`{vaultId,baseRevision,title?,entries:[{noteId,path,content,deleted?,properties?}]}`
+增量;整个 Vault 的 head 在单个事务内执行 CAS,过期 head 返回 `409`
+及 `currentRevision`,不覆盖其他客户端。删除保留 tombstone 与不可原地更新的
+正文 revision;重命名保留逻辑 ID。只读需 `read`,提交需 `read` + `write`,
+复用现有 workspace membership 和 `/Vaults/` token path 边界。
+properties 仅为有界数据,不能授予工具或身份权限。图片等原件继续使用 files API。
+
+`vault-snapshot.v1` 是独立的当前状态可迁移合同,详见
+[API、限制与错误码](docs/VAULT_SYNC.md) 和
+[JSON Schema](docs/schemas/vault-snapshot.v1.schema.json)。CLI/MCP/Web 本次不新增
+Vault 命令或独立笔记编辑器。现有 workspace bundle v1/v2 不包含 Vault 表和历史;
+完整历史仍需数据库备份。服务不运行桌面文件监听或自动合并客户端冲突;客户端必须
+拉取 head、三方合并并保留冲突副本。迁移 0027 不改变 files/auth/memory 表;降级
+到 0026 会丢弃 Vault 数据,操作前需备份。
+
## 4. 非功能需求
| 维度 | 目标 |
diff --git a/docs/TESTING.md b/docs/TESTING.md
index a3204af..480a42e 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -274,8 +274,10 @@ Required tests:
- `TestManagedAISettlementOutboxPostgres`
- `TestReleasedFileStageRetryPostgres`
- `TestDurableContextPostgres`
+- `TestVaultSyncPostgres` (two-client CAS, distinct empty notes, auth/workspace,
+ immutable history, tombstones, path swap/title and migration 27 down/up)
-Expected result: migrations reach the declared current head (currently `20`),
+Expected result: migrations reach the declared current head (currently `27`),
all rollback-state assertions pass, every named test prints `PASS`, all commands
exit `0`, and the race run reports no data race.
@@ -298,6 +300,15 @@ Run the isolated, model-free process test:
make test-acceptance
```
+The script builds temporary MinIO/mc binaries from checksum-pinned official
+source archives because Community Edition is now source-only. It uses the
+existing Go toolchain, `tar` and `sha256sum`, then builds a uniquely named test
+image on a pinned official Alpine runtime. The temporary Compose override,
+image and run directory are removed by its ownership-checked cleanup. This
+does not replace images or credentials in a production deployment. Network
+access to the fixed source archives and locked Go module dependencies is
+required; missing artifacts or checksum mismatches fail the gate.
+
The script owns a per-run Compose project, random loopback PostgreSQL/MinIO
ports, an ephemeral database/store and a new memd process on an atomically
locked, twice-preflighted loopback port. It builds the current CLI, MCP adapter
@@ -449,8 +460,8 @@ Use this table in pull requests and add implementation-specific scenarios:
| V2 | Worker processing regressions remain hermetic | `make test-worker` | Exit `0`; real-model gate explicitly skipped |
| V3 | Localization, theme, enrichment, memory, transfer and managed-embedding control surfaces work in a browser | `make test-web` | Typecheck/lint/build, the localization audit, all browser acceptance suites and managed status mapping pass |
| V4 | High-risk Go paths are race-free | `make test-race` | Exit `0`; no data-race warning |
-| V5 | Fresh schema, rollback and PostgreSQL semantics hold | `make test-integration` | Migration head and eighteen named tests pass, none skipped |
-| V6 | DB concurrency paths are race-free | `make test-integration-race` | The same eighteen tests pass under `-race` |
+| V5 | Fresh schema, rollback and PostgreSQL semantics hold | `make test-integration` | Migration head and all required named tests pass, none skipped |
+| V6 | DB concurrency paths are race-free | `make test-integration-race` | The same required tests pass under `-race` |
| V7 | Real service boundaries agree | `make test-acceptance` | HTTP, CLI and MCP share one isolated service; memory citation/provenance, bounded checkpoint listing, full checkpoint get, lifecycle and forget redaction pass |
| V8 | Five config shapes and the real adapter preserve the host-neutral MCP contract | `MEM_MCP_CERT_BINARY=... make test-agent-certification` | All fixtures and current-adapter scenarios pass with no skip |
| V9 | Multilingual visual quality meets the chosen checkpoint | Opt-in command in section 7 | All fixed ranking assertions pass |
diff --git a/docs/VAULT_SYNC.md b/docs/VAULT_SYNC.md
new file mode 100644
index 0000000..3f78447
--- /dev/null
+++ b/docs/VAULT_SYNC.md
@@ -0,0 +1,109 @@
+# Workspace Vault sync v1
+
+Implements [mem #234](https://github.com/bytefolk/mem/issues/234).
+
+This additive text-note service keeps logical identity independent of the
+existing `/v1/files` content-deduplication model. It never changes that model,
+invokes AI, or treats note properties as permissions.
+
+All endpoints use existing Bearer authentication and workspace membership.
+`X-Workspace-ID`/a bound token selects the workspace. A token must also cover
+the canonical path `/Vaults/`.
+
+* `GET /v1/vault/list` (`read`) returns
+ `{ "vaults": [{ "vaultId", "title", "revision", "updatedAt" }] }`.
+ Out-of-path Vault IDs are omitted, without revealing their count.
+* `GET /v1/vault/snapshot?vaultId=` (`read`) returns a full snapshot.
+ A new Vault returns revision `0`, empty title, `updatedAt: null`, and `[]`
+ without creating a row.
+* `POST /v1/vault/commit` (`read` + `write`) accepts
+ `{ "vaultId", "baseRevision", "title"?, "entries": [...] }`.
+ Entries are **deltas**, not a replacement of the full Vault. An entry is
+ `{ "noteId", "path", "content", "deleted"?, "properties"? }`.
+ Unlisted notes remain unchanged. `deleted: true` requires empty content.
+ Omitted properties retain existing metadata; a new note defaults to `{}`.
+ Empty entries are allowed only with a title update.
+
+Both snapshot and successful commit return:
+
+```json
+{
+ "schemaVersion": "vault-snapshot.v1",
+ "vaultId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
+ "title": "Knowledge",
+ "revision": 1,
+ "updatedAt": "2026-10-07T12:00:00Z",
+ "entries": [
+ {
+ "noteId": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
+ "path": "岗位/空白笔记.md",
+ "content": "",
+ "revision": 1,
+ "deleted": false,
+ "properties": {}
+ }
+ ]
+}
+```
+
+IDs are nonzero UUIDs. Vault IDs in requests use canonical lowercase form.
+Paths are canonical relative POSIX paths up to 512 UTF-8 bytes, preserving
+Chinese and spaces. Absolute paths, traversal, backslashes, control characters,
+Windows-forbidden filename characters and trailing dots/spaces are rejected.
+Two active notes cannot share a case-folded path. Logical notes with identical
+content, including empty content, retain separate IDs. Text is UTF-8; only
+newline, carriage return and tab control characters are accepted. Attachments
+continue to use the existing mem file API and references rather than binary
+data embedded in this text protocol.
+
+One transaction locks and compares the whole Vault head. Successful commits
+advance it exactly once and append content revisions; mutable head pointers
+select current versions. Old entry revisions cannot be updated in place.
+Delete creates a retained tombstone; it does not physically erase history.
+Rename/swap changes logical paths while preserving note IDs.
+
+* Stale head: `409 { "error": "vault_revision_conflict", "currentRevision": N }`.
+* Active path collision: `409`, `error: vault_path_conflict`.
+* Invalid body/path/identity: `400`, `error: vault_request_invalid`.
+* Capacity/body/history limit: `413`, `error: vault_limit_exceeded`.
+* Unavailable storage: `503`, `error: vault_unavailable`; success is not assumed.
+
+Clients keep their own base snapshot and local manifest. On 409 they pull the
+current head, merge nonoverlapping edits, and preserve conflicts as explicit
+copies. A lost response is reconciled through snapshot readback, never a blind
+overwrite. The service does not modify local files or promise a sync daemon.
+
+Bounds: 4 MiB HTTP commit body, 1 MiB per note, 16 KiB per properties object,
+256 UTF-8 bytes per title, 512 delta entries, 1000 heads **including tombstones**,
+16 MiB current serialized content/path/properties, 64 MiB retained entry-history
+content/path/properties, 10,000 commits and 64 Vaults per workspace. Limits
+refuse atomically; snapshots are never silently truncated. A full snapshot may
+require roughly 32 MiB on the wire due to JSON escaping; callers must use a
+bounded Vault-specific response budget rather than an 8 MiB file-card budget.
+
+Validation: unit/API tests are hermetic. `TestVaultSyncPostgres` requires an
+explicit `MEM_TEST_DB` whose database name ends in `_test`; it verifies real
+two-client races, distinct empty notes, workspace/auth isolation, tombstones,
+immutable history, swaps, title CAS and additive migration down/up.
+
+## Compatibility and portability boundary
+
+The HTTP API is the canonical implementation. This initial additive surface
+adds no CLI, MCP or standalone mem Web Vault editor; existing adapters and file
+operations retain their contracts. Clients may synchronize ordinary Markdown
+files and upload attachments through the existing file API.
+
+The current workspace-bundle v1/v2 export/import formats do not contain Vault
+heads or revision history. A Vault snapshot is a separate portable current-state
+contract (docs/schemas/vault-snapshot.v1.schema.json). It has no auth tokens,
+workspace membership or actor audit fields, and importing its properties never
+grants permissions. Restoring a snapshot into an authorized target Vault uses
+a normal CAS commit with the target head and a client-selected logical Vault
+ID. Full history remains in PostgreSQL and requires an operator database backup;
+this API does not claim to transport retained history between deployments.
+
+Migration 0027 only creates new Vault tables and indexes. Downgrading to 0026
+drops all Vault state and history, so preserve a database backup or current
+snapshot before an operator downgrade. Existing files, auth and memory tables
+are untouched. No login recovery, deployment credentials or model dependency
+is introduced.
diff --git a/docs/acceptance/VAULT_SYNC_VALIDATION.md b/docs/acceptance/VAULT_SYNC_VALIDATION.md
new file mode 100644
index 0000000..f16d0c4
--- /dev/null
+++ b/docs/acceptance/VAULT_SYNC_VALIDATION.md
@@ -0,0 +1,160 @@
+# Vault sync validation ledger
+
+Issue: [mem #234](https://github.com/bytefolk/mem/issues/234).
+Base: `c5f3fd591973dfaf0e36baecfee8cb8295c1bcb7` (`origin/main`).
+Validation date: 2026-10-08. This ledger records implementation-side evidence;
+it is not independent review approval or a deployment/release claim.
+
+Environment: Linux amd64 through WSL, Go 1.25.0, PostgreSQL 16 with the CI-pinned
+`pgvector/pgvector@sha256:00ba258a66dac104fd5171074a0084462a64a1369d8513f3d0a634e2f24d15bc`
+image. A new loopback-only container used tmpfs PostgreSQL data and the synthetic
+control database `mem_vault_publish_test`. Each integration-runner phase created
+and removed its own nonce-marked `mem_verify_*_test` database. No deployed
+database, credentials, container or user configuration was read or changed.
+
+| Check | Exact command (repository root unless specified) | Result |
+| --- | --- | --- |
+| Server/CLI/MCP compilation | `(cd server && env -u MEM_TEST_DB go build ./...)` | PASS |
+| Go static analysis | `(cd server && env -u MEM_TEST_DB go vet ./...)` | PASS |
+| All hermetic Go regressions | `(cd server && env -u MEM_TEST_DB go test -count=1 ./...)` | PASS; database tests excluded here and verified separately |
+| High-risk hermetic race checks, including Vault | `bash scripts/verify.sh race` | PASS; no data race |
+| Fresh schema, strict populated 23 → 27 upgrade, historical rollback round trips, HNSW checks and all required PostgreSQL tests | `MEM_TEST_DB= bash scripts/verify.sh integration` | PASS; migration head 27; no required test skipped |
+| All required PostgreSQL tests under race detector | `MEM_TEST_DB= bash scripts/verify.sh integration-race` | PASS; no required test skipped or data race |
+| Whitespace and schema serialization | `git diff --check`; Python `json.load` on `docs/schemas/vault-snapshot.v1.schema.json` | PASS; JSON parsing is not a claim of full JSON Schema conformance testing |
+
+The initial Windows worktree had CRLF shell/golden-fixture copies. The first
+Linux attempt failed before integration setup (`pipefail\r`) and the Go doctor
+golden comparison rejected line endings. Only those validation working copies
+were converted to the Git-equivalent LF content; the commands above were rerun
+and passed. No unrelated fixture, shell content or file-mode change is included
+in this issue's diff.
+
+## Observable Vault acceptance
+
+`TestVaultSyncPostgres` passed in both normal and race integration runners:
+
+- separate UUID identities preserve two independently synchronized empty notes;
+- two actual clients writing the same base revision have exactly one winner
+ and one `409` conflict; the losing edit never overwrites the winner;
+- real router authentication, workspace selection and read/write scopes deny
+ reader writes and keep another workspace's same Vault UUID isolated;
+- successful commits append immutable old text, advance the head once, retain
+ deletion tombstones, support path swaps and use the same CAS for title changes;
+- real discovery lists the workspace's Vault, and real HTTP stale writes return
+ the observed `currentRevision`;
+- migration `27 → 26 → 27` is exercised only on disposable data, preserving
+ existing file/auth schemas while explicitly dropping Vault state on downgrade.
+
+Hermetic `internal/vaultsync` and `internal/api` tests additionally cover
+malformed identity/path/text/properties, distinct note identity, deterministic
+delta planning, stale heads, active-path collisions, count/byte bounds,
+tombstones, unavailable storage, strict HTTP bodies, anonymous denial and
+token-path filtering for both snapshot and discovery. Property values are
+untrusted data and do not add workspace membership or tool authority.
+
+## Compatibility and honest limits
+
+- API: additive canonical HTTP routes; existing file/blob identity is unchanged.
+ The new note ID is independent of content SHA-256.
+- CLI/MCP/Web: N/A for new Vault UI/commands in this backend change. Their
+ existing behavior is covered by the complete Go suite where applicable;
+ no separate Vault adapter or desktop sync daemon is claimed.
+- Worker: N/A; no Worker, protobuf, model or queue behavior changed. The Web
+ tooling follow-up and its complete validation are recorded below.
+- Provenance/privacy: public snapshot contains current note data/properties,
+ with no authentication material, actor audit IDs or new authorization model.
+- Retry: a stale or uncertain commit is reconciled through current-head
+ readback; clients retain their base and preserve divergent edits as copies.
+ No request-level idempotency key or server-side automatic merge is claimed.
+- Recall warnings: N/A; this surface does not change search, recall or ranking.
+- Portability: `vault-snapshot.v1` transports current state. Workspace bundle
+ v1/v2 does not include Vault state; retained history needs database backup.
+- Deployment: NOT VERIFIED by this publication worktree; no production
+ migration, restart, credential issuance, release or hosted-service rollout ran.
+
+## CI baseline repair (2026-10-09)
+
+The first PR run passed Go, PostgreSQL, Worker, existing Web browser acceptance,
+deployment profiles, CodeQL and dependency review. Three gates failed for
+unchanged baseline dependencies or external artifacts, before exercising any
+Vault code:
+
+- [Linux Web audit](https://github.com/bytefolk/mem/actions/runs/37806469015/job/113411946797)
+ and [Windows audit](https://github.com/bytefolk/mem/actions/runs/37806469015/job/113411946574)
+ both reported 14 high and 2 moderate findings in the original lockfile.
+- [HTTP/CLI/MCP lifecycle](https://github.com/bytefolk/mem/actions/runs/37806468988/job/113411942810)
+ stopped at an unauthorized pull of the original public MinIO image; no
+ application service or migration had started.
+
+This follow-up keeps the audit thresholds, all lifecycle assertions and CI
+timeouts. It does not suppress vulnerabilities, ignore findings or disable a
+check.
+
+### Dependency cause and compatibility
+
+The published [braces stack-exhaustion advisory](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm)
+affects every available version through 3.0.3 and lists no patched version.
+Tailwind 3.4.19 still requires chokidar 3, fast-glob and micromatch, which retain
+that chain. typescript-eslint 7 also retains it through globby/fast-glob. These
+are active CSS and TypeScript build tools, so removing an unused package or
+applying a compatible braces patch cannot remove the finding.
+
+Use official Tailwind 4.3.3 and its same-version PostCSS plugin, plus
+typescript-eslint 8.71.1 (which supports the existing ESLint 8.57.1). Keep the
+existing theme/config palette, font, animation and shadow definitions. Use
+tailwind-merge 3.7.0 for its [supported Tailwind 4 class semantics](https://github.com/dcastil/tailwind-merge/blob/main/README.md).
+Apply compatible lockfile patches for
+[brace-expansion](https://github.com/advisories/GHSA-q2hr-2g5m-vwhr),
+[source-map-js](https://github.com/advisories/GHSA-68fv-2mgg-jv7q) and the published
+undici advisories surfaced by npm audit. No vulnerable package is renamed or
+replaced by an unreviewed fork to hide its audit identity.
+
+Follow the [official compiler upgrade contract](https://tailwindcss.com/docs/upgrade-guide):
+the existing JS theme is loaded explicitly with `@config`, and compilation uses
+`@tailwindcss/postcss`. Global compatibility declarations retain the existing
+2px small radius, 4px backdrop blur, button cursor, placeholder defaults and
+keyboard outlines. No React component or product flow is migrated.
+
+| Verification | Result |
+| --- | --- |
+| Linux Node 24.11.1 / npm 11.17.0 `npm run audit` | PASS; zero vulnerabilities at original production-moderate and all-dependencies-high thresholds |
+| Windows Node 24.19.0 / npm 11.17.0 `npm.cmd run audit` | PASS; both original thresholds and shell-free Windows invocation pass |
+| `npm run lint`, `npm run typecheck`, `npm test`, `npm run build` | PASS; 109 unit tests; pinned token bridge checks 334 contrast pairs; production build succeeds |
+| Fresh `npm ci` followed by `npm run audit` and `npm run build` | PASS; checked lockfile reproduces the zero-finding audit and same production assets |
+| `npm run test:i18n`, `test:theme`, `test:enrichment`, `test:memory`, `test:transfer`, `test:managed-embedding` | PASS; all original assertions retained |
+| New theme compatibility browser probes | PASS; dark/light RGB and opacity, radius/blur, pointer and 2px solid keyboard outline also verified under forced colors |
+
+The token-source and generated-CSS validation copies were normalized to their
+Git-equivalent LF bytes for Linux's pinned-byte check. Neither the upstream
+token hash nor generated content was changed in the commit. The existing
+production bundle-size warning remains visible.
+
+### Lifecycle artifact repair
+
+MinIO Community Edition's [official source-only distribution contract](https://github.com/minio/minio#source-only-distribution)
+no longer provides the anonymously pullable historical artifacts used by this
+test. Use the official
+[MinIO source](https://github.com/minio/minio/tree/7aac2a2c5b7c882e68c1ce017d8256be2feea27f)
+and [mc source](https://github.com/minio/mc/tree/77f82e18b5401a65958f1619df6ebb994634bd88),
+pinning both commits and archive SHA-256 before extraction. Build with existing
+Go using `-mod=readonly`, then place the binaries and their upstream licenses
+into a fixed-digest official Alpine runtime image. A temporary Compose override
+uses only the uniquely named test image, with `pull_policy: never`; the existing
+test Compose graph and production deployment assets are unchanged.
+
+| Verification | Result |
+| --- | --- |
+| `bash -n scripts/acceptance_agent_memory.sh` | PASS |
+| Official ShellCheck 0.11.0 on the final script | PASS |
+| Full `scripts/acceptance_agent_memory.sh` with the CI-exact `GOTOOLCHAIN=go1.25.0` | PASS; official-source build, fresh migration 27, healthy MinIO, real HTTP/CLI/MCP and PostgreSQL forgetting/redaction assertions all complete |
+| Ownership cleanup | PASS; run-specific containers, networks, volumes, image, directory and port lock removed; production containers unchanged |
+
+The local Windows-owned worktree used `GOFLAGS=-buildvcs=false` only while
+executing its validation binary builds, because Linux Git cannot parse its
+Windows `.git` pointer. The script's checksum-pinned upstream tarball builds
+explicitly disable VCS stamping, rather than accidentally attributing those
+sources to the enclosing checkout. The CI checkout, Go version, timeout and
+all existing service and lifecycle assertions remain unchanged.
+
+Remote CI on the repaired head must still rerun. Local pass is not substituted
+for required remote checks, CODEOWNERS approval or resolved review conversations.
diff --git a/docs/schemas/vault-snapshot.v1.schema.json b/docs/schemas/vault-snapshot.v1.schema.json
new file mode 100644
index 0000000..a66a423
--- /dev/null
+++ b/docs/schemas/vault-snapshot.v1.schema.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://github.com/bytefolk/mem/blob/main/docs/schemas/vault-snapshot.v1.schema.json",
+ "title": "Workspace Vault current-state snapshot v1",
+ "description": "Canonical GET snapshot and successful commit response. This transports current logical notes and tombstones, not auth state or retained history. UTF-8 byte bounds are additionally enforced by the server.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["schemaVersion", "vaultId", "title", "revision", "updatedAt", "entries"],
+ "properties": {
+ "schemaVersion": { "const": "vault-snapshot.v1" },
+ "vaultId": { "$ref": "#/$defs/id" },
+ "title": { "type": "string", "maxLength": 256, "$comment": "At most 256 UTF-8 bytes; empty only before the first commit." },
+ "revision": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
+ "updatedAt": { "type": ["string", "null"], "format": "date-time" },
+ "entries": {
+ "type": "array", "maxItems": 1000,
+ "items": {
+ "type": "object", "additionalProperties": false,
+ "required": ["noteId", "path", "content", "revision", "deleted", "properties"],
+ "properties": {
+ "noteId": { "$ref": "#/$defs/id" },
+ "path": { "type": "string", "minLength": 1, "maxLength": 512, "$comment": "Canonical safe relative POSIX path, at most 512 UTF-8 bytes. Active paths are case-fold unique." },
+ "content": { "type": "string", "maxLength": 1048576, "$comment": "At most 1 MiB UTF-8 bytes; only tab, CR and LF control characters are accepted." },
+ "revision": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 },
+ "deleted": { "type": "boolean" },
+ "properties": { "type": "object", "$comment": "At most 16 KiB serialized JSON. Untrusted application data, never a permission grant." }
+ },
+ "allOf": [{ "if": { "properties": { "deleted": { "const": true } } }, "then": { "properties": { "content": { "const": "" } } } }]
+ }
+ }
+ },
+ "$defs": {
+ "id": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "not": { "const": "00000000-0000-0000-0000-000000000000" } }
+ }
+}
diff --git a/scripts/acceptance_agent_memory.sh b/scripts/acceptance_agent_memory.sh
index f913556..99977cb 100755
--- a/scripts/acceptance_agent_memory.sh
+++ b/scripts/acceptance_agent_memory.sh
@@ -4,7 +4,9 @@ set -euo pipefail
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="${REPO_ROOT}/docker-compose.test.yml"
+COMPOSE_OVERRIDE=""
COMPOSE_PROJECT=""
+MINIO_IMAGE=""
PG_PORT="${MEM_ACCEPTANCE_PG_PORT:-0}"
S3_PORT="${MEM_ACCEPTANCE_S3_PORT:-0}"
HTTP_PORT_REQUESTED="${MEM_ACCEPTANCE_HTTP_PORT:-}"
@@ -33,11 +35,15 @@ require_command() {
}
compose() {
+ local -a compose_files=(-f "$COMPOSE_FILE")
+ if [[ -n "$COMPOSE_OVERRIDE" ]]; then
+ compose_files+=(-f "$COMPOSE_OVERRIDE")
+ fi
MEM_TEST_PROJECT="$COMPOSE_PROJECT" \
MEM_TEST_PG_PORT="$PG_PORT" \
MEM_TEST_S3_PORT="$S3_PORT" \
MEM_TEST_DB_NAME="$DB_NAME" \
- docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" --profile e2e "$@"
+ docker compose -p "$COMPOSE_PROJECT" "${compose_files[@]}" --profile e2e "$@"
}
cleanup() {
@@ -63,6 +69,14 @@ cleanup() {
printf 'WARNING: failed to remove Compose project %s\n' "$COMPOSE_PROJECT" >&2
cleanup_status=1
fi
+ if [[ -n "$MINIO_IMAGE" ]] &&
+ docker image inspect "$MINIO_IMAGE" >/dev/null 2>&1; then
+ if [[ "$MINIO_IMAGE" != "${COMPOSE_PROJECT}:minio" ]] ||
+ ! docker image rm "$MINIO_IMAGE" >/dev/null 2>&1; then
+ printf 'WARNING: failed to remove acceptance MinIO image\n' >&2
+ cleanup_status=1
+ fi
+ fi
if [[ -n "$E2E_DIR" && "$E2E_DIR" == "${E2E_ROOT}/acceptance."* ]]; then
if ! rm -rf -- "$E2E_DIR"; then
printf 'WARNING: failed to remove acceptance directory\n' >&2
@@ -92,6 +106,8 @@ require_command curl
require_command jq
require_command od
require_command tr
+require_command tar
+require_command sha256sum
curl_safe() {
command curl --connect-timeout 2 --max-time 20 "$@"
@@ -184,6 +200,64 @@ published_port() {
printf '%s\n' "${address##*:}"
}
+build_test_minio() {
+ # MinIO CE is source-only; its former public minio and mc images no longer
+ # pull anonymously. Build the official sources, without third-party mirrors.
+ # https://github.com/minio/minio#source-only-distribution
+ local minio_revision="7aac2a2c5b7c882e68c1ce017d8256be2feea27f"
+ local mc_revision="77f82e18b5401a65958f1619df6ebb994634bd88"
+ local build_dir="${E2E_DIR}/minio-build"
+ mkdir -p "${build_dir}/minio" "${build_dir}/mc" "${build_dir}/licenses"
+ curl --fail --silent --show-error --location --retry 2 \
+ --connect-timeout 5 --max-time 180 \
+ "https://codeload.github.com/minio/minio/tar.gz/${minio_revision}" \
+ --output "${build_dir}/minio.tar.gz"
+ curl --fail --silent --show-error --location --retry 2 \
+ --connect-timeout 5 --max-time 180 \
+ "https://codeload.github.com/minio/mc/tar.gz/${mc_revision}" \
+ --output "${build_dir}/mc.tar.gz"
+ printf '%s %s\n' \
+ '71794c2df26aad0cc99e8421c58b7aa2dd55969f979b0e7d1e931042e9fabcd6' \
+ "${build_dir}/minio.tar.gz" \
+ '167415edd21bc29f5360943dac64272aa5cda0a39f3070b15cfeca671c43d975' \
+ "${build_dir}/mc.tar.gz" | sha256sum --check --status \
+ || die "official MinIO source checksum mismatch"
+ tar -xzf "${build_dir}/minio.tar.gz" --strip-components=1 -C "${build_dir}/minio"
+ tar -xzf "${build_dir}/mc.tar.gz" --strip-components=1 -C "${build_dir}/mc"
+ (
+ cd "${build_dir}/minio"
+ CGO_ENABLED=0 GOOS=linux go build -mod=readonly -trimpath -buildvcs=false \
+ -o "${build_dir}/minio-bin" .
+ )
+ (
+ cd "${build_dir}/mc"
+ CGO_ENABLED=0 GOOS=linux go build -mod=readonly -trimpath -buildvcs=false \
+ -o "${build_dir}/mc-bin" .
+ )
+ cp "${build_dir}/minio/LICENSE" "${build_dir}/licenses/minio-LICENSE"
+ cp "${build_dir}/mc/LICENSE" "${build_dir}/licenses/mc-LICENSE"
+ cat >"${build_dir}/Dockerfile" <<'DOCKERFILE'
+FROM alpine:3.22.6@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8
+COPY minio-bin /usr/local/bin/minio
+COPY mc-bin /usr/local/bin/mc
+COPY licenses /usr/share/licenses/minio
+ENTRYPOINT ["minio"]
+DOCKERFILE
+ printf 'minio/\nmc/\n*.tar.gz\n' >"${build_dir}/.dockerignore"
+ MINIO_IMAGE="${COMPOSE_PROJECT}:minio"
+ docker build --tag "$MINIO_IMAGE" "$build_dir"
+ COMPOSE_OVERRIDE="${E2E_DIR}/minio-compose.yml"
+ cat >"$COMPOSE_OVERRIDE" <= 27 {
+ var tables int
+ if err := sqldb.QueryRowContext(ctx, `SELECT count(*) FROM pg_tables WHERE schemaname='public'
+ AND tablename IN ('vaults','vault_commits','vault_entry_revisions','vault_entry_heads')`).Scan(&tables); err != nil || tables != 4 {
+ t.Fatalf("Vault tables=%d, want=4, err=%v", tables, err)
+ }
+ }
t.Logf("PASS: strict Goose upgrade to %d; complete history and populated data preserved", version)
}
// The real startup path must accept the upgraded history unchanged.
diff --git a/server/internal/db/migrations/0027_vault_sync.sql b/server/internal/db/migrations/0027_vault_sync.sql
new file mode 100644
index 0000000..c69ad2a
--- /dev/null
+++ b/server/internal/db/migrations/0027_vault_sync.sql
@@ -0,0 +1,66 @@
+-- +goose Up
+CREATE TABLE vaults (
+ workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
+ id uuid NOT NULL,
+ title text NOT NULL CHECK (octet_length(title) BETWEEN 1 AND 256),
+ revision bigint NOT NULL DEFAULT 0 CHECK (revision BETWEEN 0 AND 9007199254740991),
+ updated_at timestamptz NOT NULL DEFAULT now(),
+ PRIMARY KEY (workspace_id, id)
+);
+
+CREATE TABLE vault_commits (
+ workspace_id uuid NOT NULL,
+ vault_id uuid NOT NULL,
+ revision bigint NOT NULL CHECK (revision BETWEEN 1 AND 9007199254740991),
+ title text NOT NULL,
+ actor_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ PRIMARY KEY (workspace_id, vault_id, revision),
+ FOREIGN KEY (workspace_id, vault_id) REFERENCES vaults(workspace_id, id) ON DELETE CASCADE
+);
+
+CREATE TABLE vault_entry_revisions (
+ workspace_id uuid NOT NULL,
+ vault_id uuid NOT NULL,
+ note_id uuid NOT NULL,
+ revision bigint NOT NULL,
+ path text NOT NULL CHECK (octet_length(path) BETWEEN 1 AND 512),
+ content text NOT NULL CHECK (octet_length(content) <= 1048576),
+ properties jsonb NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(properties) = 'object' AND octet_length(properties::text) <= 16384),
+ deleted boolean NOT NULL DEFAULT false,
+ CHECK (NOT deleted OR content = ''),
+ PRIMARY KEY (workspace_id, vault_id, note_id, revision),
+ FOREIGN KEY (workspace_id, vault_id, revision) REFERENCES vault_commits(workspace_id, vault_id, revision) ON DELETE CASCADE
+);
+
+CREATE TABLE vault_entry_heads (
+ workspace_id uuid NOT NULL,
+ vault_id uuid NOT NULL,
+ note_id uuid NOT NULL,
+ revision bigint NOT NULL,
+ path text NOT NULL,
+ deleted boolean NOT NULL,
+ PRIMARY KEY (workspace_id, vault_id, note_id),
+ FOREIGN KEY (workspace_id, vault_id, note_id, revision)
+ REFERENCES vault_entry_revisions(workspace_id, vault_id, note_id, revision) ON DELETE CASCADE
+);
+-- Blob identity is never note identity. Separate empty notes keep separate IDs.
+CREATE UNIQUE INDEX vault_entry_active_path ON vault_entry_heads(workspace_id, vault_id, lower(path)) WHERE NOT deleted;
+
+-- Content revisions are append-only; head pointers are the mutable projection.
+-- +goose StatementBegin
+CREATE FUNCTION reject_vault_entry_revision_update() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ RAISE EXCEPTION 'Vault entry revisions are immutable' USING ERRCODE = '23514';
+END;
+$$;
+-- +goose StatementEnd
+CREATE TRIGGER vault_entry_revisions_immutable BEFORE UPDATE ON vault_entry_revisions
+ FOR EACH ROW EXECUTE FUNCTION reject_vault_entry_revision_update();
+
+-- +goose Down
+DROP TABLE vault_entry_heads;
+DROP TABLE vault_entry_revisions;
+DROP FUNCTION reject_vault_entry_revision_update();
+DROP TABLE vault_commits;
+DROP TABLE vaults;
diff --git a/server/internal/vaultsync/README.md b/server/internal/vaultsync/README.md
new file mode 100644
index 0000000..5ebeee1
--- /dev/null
+++ b/server/internal/vaultsync/README.md
@@ -0,0 +1,6 @@
+# Workspace Vault sync
+
+The canonical wire contract, bounds and compatibility boundaries are documented
+in [docs/VAULT_SYNC.md](../../../docs/VAULT_SYNC.md).
+
+The snapshot schema is [vault-snapshot.v1](../../../docs/schemas/vault-snapshot.v1.schema.json).
diff --git a/server/internal/vaultsync/service.go b/server/internal/vaultsync/service.go
new file mode 100644
index 0000000..402eb8c
--- /dev/null
+++ b/server/internal/vaultsync/service.go
@@ -0,0 +1,470 @@
+// Package vaultsync stores workspace-scoped text Vaults with append-only
+// revisions and an atomic Vault-wide compare-and-swap head. File/blob upload
+// deduplication is deliberately outside this logical note identity model.
+package vaultsync
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "path"
+ "sort"
+ "strings"
+ "time"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgconn"
+ "github.com/jackc/pgx/v5/pgxpool"
+
+ "github.com/PeterGuy326/mem/server/internal/workspacelock"
+)
+
+const (
+ SchemaVersion = "vault-snapshot.v1"
+ MaxFileBytes = 1 << 20
+ MaxPropertiesBytes = 16 << 10
+ MaxCurrentBytes = 16 << 20
+ MaxHistoryBytes = 64 << 20
+ MaxEntries = 1000
+ MaxMutationEntries = 512
+ MaxVaults = 64
+ MaxCommits = 10000
+ MaxSafeRevision int64 = 9007199254740991
+)
+
+var (
+ ErrInvalid = errors.New("invalid Vault request")
+ ErrLimit = errors.New("Vault limit exceeded")
+ ErrPathConflict = errors.New("Vault path conflict")
+ ErrUnavailable = errors.New("Vault sync is unavailable")
+)
+
+type RevisionConflict struct{ CurrentRevision int64 }
+
+func (e *RevisionConflict) Error() string { return "Vault revision conflict" }
+
+type Mutation struct {
+ NoteID uuid.UUID `json:"noteId"`
+ Path string `json:"path"`
+ Content string `json:"content"`
+ Deleted bool `json:"deleted,omitempty"`
+ Properties json.RawMessage `json:"properties,omitempty"`
+}
+
+type Entry struct {
+ NoteID uuid.UUID `json:"noteId"`
+ Path string `json:"path"`
+ Content string `json:"content"`
+ Revision int64 `json:"revision"`
+ Deleted bool `json:"deleted"`
+ Properties json.RawMessage `json:"properties"`
+}
+
+type Snapshot struct {
+ SchemaVersion string `json:"schemaVersion"`
+ VaultID uuid.UUID `json:"vaultId"`
+ Title string `json:"title"`
+ Revision int64 `json:"revision"`
+ UpdatedAt *time.Time `json:"updatedAt"`
+ Entries []Entry `json:"entries"`
+}
+
+type Summary struct {
+ VaultID uuid.UUID `json:"vaultId"`
+ Title string `json:"title"`
+ Revision int64 `json:"revision"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+type CommitCommand struct {
+ WorkspaceID uuid.UUID
+ ActorUserID uuid.UUID
+ VaultID uuid.UUID
+ BaseRevision int64
+ Title *string
+ Entries []Mutation
+}
+
+type Service struct{ pool *pgxpool.Pool }
+
+func New(pool *pgxpool.Pool) *Service { return &Service{pool: pool} }
+func ScopePath(id uuid.UUID) string { return "/Vaults/" + id.String() }
+
+func ParseVaultID(raw string) (uuid.UUID, error) {
+ id, err := uuid.Parse(raw)
+ if err != nil || id == uuid.Nil || id.String() != raw {
+ return uuid.Nil, ErrInvalid
+ }
+ return id, nil
+}
+
+func ValidatePath(value string) error {
+ if !utf8.ValidString(value) || len(value) == 0 || len(value) > 512 || path.IsAbs(value) || path.Clean(value) != value ||
+ strings.ContainsAny(value, "\\:*?\"<>|") || strings.IndexFunc(value, unicode.IsControl) >= 0 {
+ return ErrInvalid
+ }
+ for _, part := range strings.Split(value, "/") {
+ if part == "." || part == ".." || strings.HasSuffix(part, ".") || strings.HasSuffix(part, " ") {
+ return ErrInvalid
+ }
+ }
+ return nil
+}
+
+func normalizeProperties(raw json.RawMessage) (json.RawMessage, error) {
+ if len(raw) == 0 {
+ return nil, nil
+ } // Existing metadata is preserved when omitted.
+ if len(raw) > MaxPropertiesBytes || !utf8.Valid(raw) {
+ return nil, ErrLimit
+ }
+ var value map[string]any
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.UseNumber()
+ if err := decoder.Decode(&value); err != nil || value == nil {
+ return nil, ErrInvalid
+ }
+ if containsZero(value) {
+ return nil, ErrInvalid
+ }
+ if err := decoder.Decode(new(any)); err != io.EOF {
+ return nil, ErrInvalid
+ }
+ encoded, err := json.Marshal(value)
+ if err != nil {
+ return nil, ErrInvalid
+ }
+ // PostgreSQL jsonb may add spaces between keys/values; this bound applies
+ // to its actual persisted representation as well as the compact wire value.
+ var indented bytes.Buffer
+ if err := json.Indent(&indented, encoded, "", ""); err != nil || indented.Len() > MaxPropertiesBytes {
+ return nil, ErrLimit
+ }
+ return encoded, nil
+}
+
+func containsZero(value any) bool {
+ switch typed := value.(type) {
+ case string:
+ return strings.ContainsRune(typed, 0)
+ case map[string]any:
+ for key, item := range typed {
+ if strings.ContainsRune(key, 0) || containsZero(item) {
+ return true
+ }
+ }
+ case []any:
+ for _, item := range typed {
+ if containsZero(item) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func NormalizeCommand(cmd CommitCommand) (CommitCommand, error) {
+ if cmd.WorkspaceID == uuid.Nil || cmd.ActorUserID == uuid.Nil || cmd.VaultID == uuid.Nil || cmd.BaseRevision < 0 ||
+ cmd.BaseRevision >= MaxSafeRevision || len(cmd.Entries) > MaxMutationEntries || (len(cmd.Entries) == 0 && cmd.Title == nil) {
+ return cmd, ErrInvalid
+ }
+ if cmd.Title != nil && (!utf8.ValidString(*cmd.Title) || len(*cmd.Title) == 0 || len(*cmd.Title) > 256 ||
+ strings.TrimSpace(*cmd.Title) == "" || strings.IndexFunc(*cmd.Title, unicode.IsControl) >= 0) {
+ return cmd, ErrInvalid
+ }
+ seen := make(map[uuid.UUID]bool)
+ cmd.Entries = append([]Mutation(nil), cmd.Entries...)
+ for i, entry := range cmd.Entries {
+ if entry.NoteID == uuid.Nil || seen[entry.NoteID] || ValidatePath(entry.Path) != nil || !utf8.ValidString(entry.Content) ||
+ strings.IndexFunc(entry.Content, func(r rune) bool { return unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' }) >= 0 || (entry.Deleted && entry.Content != "") {
+ return cmd, ErrInvalid
+ }
+ if len(entry.Content) > MaxFileBytes {
+ return cmd, ErrLimit
+ }
+ seen[entry.NoteID] = true
+ properties, err := normalizeProperties(entry.Properties)
+ if err != nil {
+ return cmd, err
+ }
+ cmd.Entries[i].Properties = properties
+ }
+ return cmd, nil
+}
+
+// plan applies a delta without silently discarding notes, tombstones or a
+// conflict. Properties are data and can never grant tools or membership.
+func plan(current Snapshot, cmd CommitCommand) (Snapshot, error) {
+ if current.Revision != cmd.BaseRevision {
+ return Snapshot{}, &RevisionConflict{current.Revision}
+ }
+ entries := make(map[uuid.UUID]Entry, len(current.Entries)+len(cmd.Entries))
+ for _, entry := range current.Entries {
+ entries[entry.NoteID] = entry
+ }
+ next := current
+ next.Revision++
+ if cmd.Title != nil {
+ next.Title = *cmd.Title
+ }
+ if next.Title == "" {
+ next.Title = "Vault " + cmd.VaultID.String()[:8]
+ }
+ for _, mutation := range cmd.Entries {
+ properties := mutation.Properties
+ if len(properties) == 0 {
+ properties = entries[mutation.NoteID].Properties
+ }
+ if len(properties) == 0 {
+ properties = json.RawMessage(`{}`)
+ }
+ entries[mutation.NoteID] = Entry{NoteID: mutation.NoteID, Path: mutation.Path, Content: mutation.Content,
+ Deleted: mutation.Deleted, Properties: append(json.RawMessage(nil), properties...), Revision: next.Revision}
+ }
+ if len(entries) > MaxEntries {
+ return Snapshot{}, ErrLimit
+ }
+ paths := make(map[string]bool)
+ total := 0
+ next.Entries = make([]Entry, 0, len(entries))
+ for _, entry := range entries {
+ if !entry.Deleted {
+ key := strings.ToLower(entry.Path)
+ if paths[key] {
+ return Snapshot{}, ErrPathConflict
+ }
+ paths[key] = true
+ }
+ total += len(entry.Content) + len(entry.Properties) + len(entry.Path)
+ if total > MaxCurrentBytes {
+ return Snapshot{}, ErrLimit
+ }
+ next.Entries = append(next.Entries, entry)
+ }
+ sort.Slice(next.Entries, func(i, j int) bool {
+ if next.Entries[i].Path == next.Entries[j].Path {
+ return next.Entries[i].NoteID.String() < next.Entries[j].NoteID.String()
+ }
+ return next.Entries[i].Path < next.Entries[j].Path
+ })
+ return next, nil
+}
+
+type querier interface {
+ Query(context.Context, string, ...any) (pgx.Rows, error)
+ QueryRow(context.Context, string, ...any) pgx.Row
+}
+
+func readSnapshot(ctx context.Context, q querier, workspaceID, vaultID uuid.UUID) (Snapshot, error) {
+ snapshot := Snapshot{SchemaVersion: SchemaVersion, VaultID: vaultID, Entries: []Entry{}}
+ var updated time.Time
+ err := q.QueryRow(ctx, `SELECT title, revision, updated_at FROM vaults WHERE workspace_id=$1 AND id=$2`, workspaceID, vaultID).
+ Scan(&snapshot.Title, &snapshot.Revision, &updated)
+ if errors.Is(err, pgx.ErrNoRows) {
+ return snapshot, nil
+ }
+ if err != nil {
+ return Snapshot{}, err
+ }
+ snapshot.UpdatedAt = &updated
+ rows, err := q.Query(ctx, `SELECT r.note_id, r.path, r.content, r.revision, r.deleted, r.properties
+ FROM vault_entry_heads h JOIN vault_entry_revisions r USING(workspace_id,vault_id,note_id,revision)
+ WHERE h.workspace_id=$1 AND h.vault_id=$2 ORDER BY r.path COLLATE "C", r.note_id LIMIT $3`, workspaceID, vaultID, MaxEntries+1)
+ if err != nil {
+ return Snapshot{}, err
+ }
+ defer rows.Close()
+ for rows.Next() {
+ var entry Entry
+ if err := rows.Scan(&entry.NoteID, &entry.Path, &entry.Content, &entry.Revision, &entry.Deleted, &entry.Properties); err != nil {
+ return Snapshot{}, err
+ }
+ snapshot.Entries = append(snapshot.Entries, entry)
+ }
+ if len(snapshot.Entries) > MaxEntries {
+ return Snapshot{}, ErrLimit
+ }
+ return snapshot, rows.Err()
+}
+
+func (s *Service) Snapshot(ctx context.Context, workspaceID, vaultID uuid.UUID) (Snapshot, error) {
+ if s == nil || s.pool == nil {
+ return Snapshot{}, ErrUnavailable
+ }
+ if workspaceID == uuid.Nil || vaultID == uuid.Nil {
+ return Snapshot{}, ErrInvalid
+ }
+ tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly})
+ if err != nil {
+ return Snapshot{}, err
+ }
+ defer func() { _ = tx.Rollback(ctx) }()
+ snapshot, err := readSnapshot(ctx, tx, workspaceID, vaultID)
+ if err != nil {
+ return Snapshot{}, err
+ }
+ if err := tx.Commit(ctx); err != nil {
+ return Snapshot{}, err
+ }
+ return snapshot, nil
+}
+
+func (s *Service) List(ctx context.Context, workspaceID uuid.UUID) ([]Summary, error) {
+ if s == nil || s.pool == nil {
+ return nil, ErrUnavailable
+ }
+ if workspaceID == uuid.Nil {
+ return nil, ErrInvalid
+ }
+ rows, err := s.pool.Query(ctx, `SELECT id,title,revision,updated_at FROM vaults WHERE workspace_id=$1 ORDER BY updated_at DESC,id LIMIT $2`, workspaceID, MaxVaults+1)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ result := []Summary{}
+ for rows.Next() {
+ var item Summary
+ if err := rows.Scan(&item.VaultID, &item.Title, &item.Revision, &item.UpdatedAt); err != nil {
+ return nil, err
+ }
+ result = append(result, item)
+ }
+ if len(result) > MaxVaults {
+ return nil, ErrLimit
+ }
+ return result, rows.Err()
+}
+
+func (s *Service) Commit(ctx context.Context, raw CommitCommand) (Snapshot, error) {
+ if s == nil || s.pool == nil {
+ return Snapshot{}, ErrUnavailable
+ }
+ cmd, err := NormalizeCommand(raw)
+ if err != nil {
+ return Snapshot{}, err
+ }
+ tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return Snapshot{}, err
+ }
+ defer func() { _ = tx.Rollback(ctx) }()
+ // Take the existing stronger workspace coordination lock first. Ordinary
+ // file writers retain KEY SHARE compatibility, while Vault commits and
+ // creation quotas serialize across distinct Vault IDs in this workspace.
+ if _, err := workspacelock.ForAIProfileCoordination(ctx, tx, cmd.WorkspaceID); err != nil {
+ return Snapshot{}, err
+ }
+ var count int
+ if err := tx.QueryRow(ctx, `SELECT count(*) FROM vaults WHERE workspace_id=$1`, cmd.WorkspaceID).Scan(&count); err != nil {
+ return Snapshot{}, err
+ }
+ var exists bool
+ if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM vaults WHERE workspace_id=$1 AND id=$2)`, cmd.WorkspaceID, cmd.VaultID).Scan(&exists); err != nil {
+ return Snapshot{}, err
+ }
+ if !exists && count >= MaxVaults {
+ return Snapshot{}, ErrLimit
+ }
+ defaultTitle := "Vault " + cmd.VaultID.String()[:8]
+ if _, err := tx.Exec(ctx, `INSERT INTO vaults(workspace_id,id,title) VALUES($1,$2,$3) ON CONFLICT DO NOTHING`, cmd.WorkspaceID, cmd.VaultID, defaultTitle); err != nil {
+ return Snapshot{}, err
+ }
+ var revision int64
+ if err := tx.QueryRow(ctx, `SELECT revision FROM vaults WHERE workspace_id=$1 AND id=$2 FOR UPDATE`, cmd.WorkspaceID, cmd.VaultID).Scan(&revision); err != nil {
+ return Snapshot{}, err
+ }
+ if revision != cmd.BaseRevision {
+ return Snapshot{}, &RevisionConflict{revision}
+ }
+ current, err := readSnapshot(ctx, tx, cmd.WorkspaceID, cmd.VaultID)
+ if err != nil {
+ return Snapshot{}, err
+ }
+ next, err := plan(current, cmd)
+ if err != nil {
+ return Snapshot{}, err
+ }
+ var historyBytes int64
+ var commits int
+ if err := tx.QueryRow(ctx, `SELECT COALESCE(sum(octet_length(content)+octet_length(properties::text)+octet_length(path)),0)
+ FROM vault_entry_revisions WHERE workspace_id=$1 AND vault_id=$2`, cmd.WorkspaceID, cmd.VaultID).Scan(&historyBytes); err != nil {
+ return Snapshot{}, err
+ }
+ if err := tx.QueryRow(ctx, `SELECT count(*) FROM vault_commits WHERE workspace_id=$1 AND vault_id=$2`, cmd.WorkspaceID, cmd.VaultID).Scan(&commits); err != nil {
+ return Snapshot{}, err
+ }
+ changed := make(map[uuid.UUID]Entry)
+ for _, entry := range next.Entries {
+ if entry.Revision == next.Revision {
+ changed[entry.NoteID] = entry
+ historyBytes += int64(len(entry.Content) + len(entry.Path) + len(entry.Properties))
+ }
+ }
+ if historyBytes > MaxHistoryBytes || commits >= MaxCommits {
+ return Snapshot{}, ErrLimit
+ }
+ updated := time.Now().UTC()
+ if _, err := tx.Exec(ctx, `INSERT INTO vault_commits(workspace_id,vault_id,revision,title,actor_user_id,created_at) VALUES($1,$2,$3,$4,$5,$6)`,
+ cmd.WorkspaceID, cmd.VaultID, next.Revision, next.Title, cmd.ActorUserID, updated); err != nil {
+ return Snapshot{}, err
+ }
+ ids := make([]uuid.UUID, 0, len(changed))
+ for id := range changed {
+ ids = append(ids, id)
+ }
+ // Delete only mutable pointers inside the atomic transaction. This permits
+ // swaps/renames without a transient uniqueness conflict; history remains.
+ if _, err := tx.Exec(ctx, `DELETE FROM vault_entry_heads WHERE workspace_id=$1 AND vault_id=$2 AND note_id=ANY($3::uuid[])`, cmd.WorkspaceID, cmd.VaultID, ids); err != nil {
+ return Snapshot{}, err
+ }
+ for _, entry := range changed {
+ if _, err := tx.Exec(ctx, `INSERT INTO vault_entry_revisions(workspace_id,vault_id,note_id,revision,path,content,deleted,properties)
+ VALUES($1,$2,$3,$4,$5,$6,$7,$8::jsonb)`, cmd.WorkspaceID, cmd.VaultID, entry.NoteID, entry.Revision, entry.Path, entry.Content, entry.Deleted, string(entry.Properties)); err != nil {
+ return Snapshot{}, classifyWriteError(err)
+ }
+ if _, err := tx.Exec(ctx, `INSERT INTO vault_entry_heads(workspace_id,vault_id,note_id,revision,path,deleted) VALUES($1,$2,$3,$4,$5,$6)`,
+ cmd.WorkspaceID, cmd.VaultID, entry.NoteID, entry.Revision, entry.Path, entry.Deleted); err != nil {
+ return Snapshot{}, classifyWriteError(err)
+ }
+ }
+ // Enforce quotas against PostgreSQL's actual jsonb representation, including
+ // its spacing, rather than estimating persisted bytes from compact JSON.
+ var persistedCurrent, persistedHistory int64
+ if err := tx.QueryRow(ctx, `SELECT COALESCE(sum(octet_length(r.content)+octet_length(r.properties::text)+octet_length(r.path)),0)
+ FROM vault_entry_heads h JOIN vault_entry_revisions r USING(workspace_id,vault_id,note_id,revision)
+ WHERE h.workspace_id=$1 AND h.vault_id=$2`, cmd.WorkspaceID, cmd.VaultID).Scan(&persistedCurrent); err != nil {
+ return Snapshot{}, err
+ }
+ if err := tx.QueryRow(ctx, `SELECT COALESCE(sum(octet_length(content)+octet_length(properties::text)+octet_length(path)),0)
+ FROM vault_entry_revisions WHERE workspace_id=$1 AND vault_id=$2`, cmd.WorkspaceID, cmd.VaultID).Scan(&persistedHistory); err != nil {
+ return Snapshot{}, err
+ }
+ if persistedCurrent > MaxCurrentBytes || persistedHistory > MaxHistoryBytes {
+ return Snapshot{}, ErrLimit
+ }
+ if _, err := tx.Exec(ctx, `UPDATE vaults SET title=$3,revision=$4,updated_at=$5 WHERE workspace_id=$1 AND id=$2`, cmd.WorkspaceID, cmd.VaultID, next.Title, next.Revision, updated); err != nil {
+ return Snapshot{}, err
+ }
+ if err := tx.Commit(ctx); err != nil {
+ return Snapshot{}, err
+ }
+ next.UpdatedAt = &updated
+ return next, nil
+}
+
+func classifyWriteError(err error) error {
+ var pgError *pgconn.PgError
+ if errors.As(err, &pgError) && pgError.Code == "23505" {
+ return ErrPathConflict
+ }
+ if errors.As(err, &pgError) && pgError.Code == "23514" {
+ return ErrLimit
+ }
+ return fmt.Errorf("Vault write failed: %w", err)
+}
diff --git a/server/internal/vaultsync/service_integration_test.go b/server/internal/vaultsync/service_integration_test.go
new file mode 100644
index 0000000..c56086a
--- /dev/null
+++ b/server/internal/vaultsync/service_integration_test.go
@@ -0,0 +1,235 @@
+package vaultsync_test
+
+import (
+ "bytes"
+ "context"
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "io"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5/pgxpool"
+ "github.com/pressly/goose/v3"
+
+ "github.com/PeterGuy326/mem/server/internal/api"
+ "github.com/PeterGuy326/mem/server/internal/auth"
+ memdb "github.com/PeterGuy326/mem/server/internal/db"
+ "github.com/PeterGuy326/mem/server/internal/vaultsync"
+ "github.com/PeterGuy326/mem/server/internal/workspace"
+)
+
+// Uses only an explicitly selected isolated _test database. No production
+// migration, credential recovery, or container restart occurs in this test.
+func TestVaultSyncPostgres(t *testing.T) {
+ dsn := os.Getenv("MEM_TEST_DB")
+ if dsn == "" {
+ t.Skip("MEM_TEST_DB not set; real PostgreSQL Vault verification was not executed")
+ }
+ config, err := pgxpool.ParseConfig(dsn)
+ if err != nil {
+ t.Fatal("invalid isolated test database configuration")
+ }
+ if !strings.HasSuffix(config.ConnConfig.Database, "_test") {
+ t.Fatal("refusing to modify a database whose name does not end in _test")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
+ defer cancel()
+ database, err := memdb.Open(ctx, dsn)
+ if err != nil {
+ t.Fatal("could not connect to isolated test database")
+ }
+ t.Cleanup(database.Close)
+ if err := database.Migrate(ctx); err != nil {
+ t.Fatalf("isolated migrations: %v", err)
+ }
+ createTenant := func() (uuid.UUID, uuid.UUID) {
+ user, ws := uuid.New(), uuid.New()
+ if _, err := database.Pool.Exec(ctx, `INSERT INTO users(id,email,password_hash) VALUES($1,$2,'test-hash')`, user, user.String()+"@vault.test"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := database.Pool.Exec(ctx, `INSERT INTO workspaces(id,name,resource_owner_user_id) VALUES($1,'Vault test',$2)`, ws, user); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := database.Pool.Exec(ctx, `INSERT INTO workspace_memberships(workspace_id,user_id,role) VALUES($1,$2,'owner')`, ws, user); err != nil {
+ t.Fatal(err)
+ }
+ return user, ws
+ }
+ userA, wsA := createTenant()
+ userB, wsB := createTenant()
+ t.Cleanup(func() {
+ cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ _, _ = database.Pool.Exec(cleanupCtx, `DELETE FROM users WHERE id=$1 OR id=$2`, userA, userB)
+ })
+ service := vaultsync.New(database.Pool)
+ vault, noteA, noteB := uuid.New(), uuid.New(), uuid.New()
+ command := vaultsync.CommitCommand{WorkspaceID: wsA, ActorUserID: userA, VaultID: vault, Entries: []vaultsync.Mutation{
+ {NoteID: noteA, Path: "岗位/第一份 空笔记.md", Content: ""}, {NoteID: noteB, Path: "共享/第二份 空笔记.md", Content: ""}}}
+ empty, err := service.Snapshot(ctx, wsA, vault)
+ if err != nil || empty.Revision != 0 || len(empty.Entries) != 0 {
+ t.Fatal("new Vault snapshot", err)
+ }
+ first, err := service.Commit(ctx, command)
+ if err != nil || first.Revision != 1 || len(first.Entries) != 2 {
+ t.Fatalf("two independent empty notes: %+v %v", first, err)
+ }
+ // Two clients read the same head, then race a different edit. Exactly one
+ // succeeds; the other gets a conflict without overwriting the winner.
+ var wg sync.WaitGroup
+ var lock sync.Mutex
+ successes, conflicts := 0, 0
+ for _, text := range []string{"客户端 A 内容", "客户端 B 内容"} {
+ wg.Add(1)
+ go func(text string) {
+ defer wg.Done()
+ cmd := command
+ cmd.BaseRevision = 1
+ cmd.Entries = []vaultsync.Mutation{{NoteID: noteA, Path: command.Entries[0].Path, Content: text}}
+ _, err := service.Commit(ctx, cmd)
+ lock.Lock()
+ defer lock.Unlock()
+ var conflict *vaultsync.RevisionConflict
+ if err == nil {
+ successes++
+ } else if errors.As(err, &conflict) && conflict.CurrentRevision == 2 {
+ conflicts++
+ } else {
+ t.Errorf("unexpected concurrent result: %v", err)
+ }
+ }(text)
+ }
+ wg.Wait()
+ if successes != 1 || conflicts != 1 {
+ t.Fatalf("CAS race successes=%d conflicts=%d", successes, conflicts)
+ }
+ second, err := service.Snapshot(ctx, wsA, vault)
+ if err != nil || second.Revision != 2 || len(second.Entries) != 2 {
+ t.Fatal("consistent snapshot after race", err)
+ }
+ other, err := service.Snapshot(ctx, wsB, vault)
+ if err != nil || other.Revision != 0 || len(other.Entries) != 0 {
+ t.Fatal("another workspace saw the Vault", err)
+ }
+ // Old text revisions remain unchanged and cannot be updated in place.
+ var original string
+ if err := database.Pool.QueryRow(ctx, `SELECT content FROM vault_entry_revisions WHERE workspace_id=$1 AND vault_id=$2 AND note_id=$3 AND revision=1`, wsA, vault, noteA).Scan(&original); err != nil || original != "" {
+ t.Fatal("original revision changed", err)
+ }
+ if _, err := database.Pool.Exec(ctx, `UPDATE vault_entry_revisions SET content='rewrite' WHERE workspace_id=$1 AND vault_id=$2 AND note_id=$3 AND revision=1`, wsA, vault, noteA); err == nil {
+ t.Fatal("immutable revision accepted UPDATE")
+ }
+ command.BaseRevision = 2
+ command.Entries = []vaultsync.Mutation{{NoteID: noteA, Path: "岗位/第一份 空笔记.md", Deleted: true}}
+ third, err := service.Commit(ctx, command)
+ if err != nil || third.Revision != 3 || len(third.Entries) != 2 {
+ t.Fatal("delete tombstone commit", err)
+ }
+ for _, entry := range third.Entries {
+ if entry.NoteID == noteA && (!entry.Deleted || entry.Content != "") {
+ t.Fatal("missing tombstone")
+ }
+ }
+ // Test swap/rename under the actual unique index, then title-only commit.
+ command.BaseRevision = 3
+ command.Entries = []vaultsync.Mutation{{NoteID: noteA, Path: "共享/第二份 空笔记.md", Content: "恢复"}, {NoteID: noteB, Path: "岗位/第一份 空笔记.md", Content: "交换"}}
+ fourth, err := service.Commit(ctx, command)
+ if err != nil || fourth.Revision != 4 {
+ t.Fatal("atomic path swap", err)
+ }
+ title := "组织知识库"
+ command.BaseRevision = 4
+ command.Entries = nil
+ command.Title = &title
+ fifth, err := service.Commit(ctx, command)
+ if err != nil || fifth.Revision != 5 || fifth.Title != title {
+ t.Fatal("title CAS", err)
+ }
+ listed, err := service.List(ctx, wsA)
+ if err != nil || len(listed) != 1 || listed[0].VaultID != vault {
+ t.Fatal("Vault discovery", err)
+ }
+ // Real HTTP + canonical auth + workspace middleware use sandbox-only tokens.
+ authService := auth.New(database.Pool)
+ ttl := time.Now().Add(time.Minute)
+ writer, _, err := authService.CreateToken(ctx, userA, &wsA, "vault-test-writer", []string{auth.ScopeRead, auth.ScopeWrite}, nil, &ttl, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ reader, _, err := authService.CreateToken(ctx, userA, &wsA, "vault-test-reader", []string{auth.ScopeRead}, nil, &ttl, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ foreign, _, err := authService.CreateToken(ctx, userB, &wsB, "vault-test-foreign", []string{auth.ScopeRead}, nil, &ttl, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ server := httptest.NewServer((&api.Server{Auth: authService, Workspace: workspace.New(database.Pool), VaultSync: service, Log: slog.New(slog.NewTextHandler(io.Discard, nil))}).Router())
+ defer server.Close()
+ request := func(method, route, token string, body any) (int, []byte) {
+ t.Helper()
+ var encoded []byte
+ if body != nil {
+ encoded, err = json.Marshal(body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ req, err := http.NewRequestWithContext(ctx, method, server.URL+route, bytes.NewReader(encoded))
+ if err != nil {
+ t.Fatal(err)
+ }
+ req.Header.Set("Authorization", "Bearer "+token)
+ req.Header.Set("Content-Type", "application/json")
+ response, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer response.Body.Close()
+ payload, err := io.ReadAll(response.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return response.StatusCode, payload
+ }
+ status, body := request("GET", "/v1/vault/snapshot?vaultId="+vault.String(), reader, nil)
+ if status != 200 || !bytes.Contains(body, []byte("组织知识库")) {
+ t.Fatal("authenticated snapshot HTTP", status)
+ }
+ status, _ = request("POST", "/v1/vault/commit", reader, map[string]any{"vaultId": vault, "baseRevision": 5, "title": "拒绝只读写入", "entries": []any{}})
+ if status != 403 {
+ t.Fatal("read-only credential could commit", status)
+ }
+ status, body = request("GET", "/v1/vault/snapshot?vaultId="+vault.String(), foreign, nil)
+ if status != 200 || !bytes.Contains(body, []byte(`"revision":0`)) || bytes.Contains(body, []byte("组织知识库")) {
+ t.Fatal("HTTP cross-workspace isolation", status)
+ }
+ status, body = request("POST", "/v1/vault/commit", writer, map[string]any{"vaultId": vault, "baseRevision": 1, "title": "stale", "entries": []any{}})
+ if status != 409 || !bytes.Contains(body, []byte(`"currentRevision":5`)) {
+ t.Fatalf("HTTP CAS conflict: %d %s", status, body)
+ }
+ // The additive migration is reversible on a disposable test deployment;
+ // dropping Vault tables does not change existing files or auth tables.
+ server.Close()
+ sqlDB, err := sql.Open("pgx", dsn)
+ if err != nil {
+ t.Fatal("could not open isolated migration rehearsal")
+ }
+ defer sqlDB.Close()
+ goose.SetBaseFS(os.DirFS("../db"))
+ if err := goose.DownToContext(ctx, sqlDB, "migrations", 26); err != nil {
+ t.Fatalf("isolated Vault down migration: %v", err)
+ }
+ if err := database.Migrate(ctx); err != nil {
+ t.Fatalf("isolated Vault up migration: %v", err)
+ }
+}
diff --git a/server/internal/vaultsync/service_test.go b/server/internal/vaultsync/service_test.go
new file mode 100644
index 0000000..cd81ccb
--- /dev/null
+++ b/server/internal/vaultsync/service_test.go
@@ -0,0 +1,150 @@
+package vaultsync
+
+import (
+ "encoding/json"
+ "errors"
+ "strings"
+ "testing"
+
+ "github.com/google/uuid"
+)
+
+func validCommand() CommitCommand {
+ return CommitCommand{WorkspaceID: uuid.New(), ActorUserID: uuid.New(), VaultID: uuid.New(),
+ Entries: []Mutation{{NoteID: uuid.New(), Path: "知识/空白笔记.md", Content: ""}}}
+}
+
+func TestLogicalIdentityEmptyNotesAndCAS(t *testing.T) {
+ cmd := validCommand()
+ cmd.Entries = append(cmd.Entries, Mutation{NoteID: uuid.New(), Path: "知识/另一个空笔记.md", Content: ""})
+ empty := Snapshot{SchemaVersion: SchemaVersion, VaultID: cmd.VaultID, Entries: []Entry{}}
+ first, err := plan(empty, cmd)
+ if err != nil || first.Revision != 1 || len(first.Entries) != 2 || first.Entries[0].NoteID == first.Entries[1].NoteID {
+ t.Fatalf("separate empty note identities: %+v, %v", first, err)
+ }
+ _, err = plan(first, cmd)
+ var conflict *RevisionConflict
+ if !errors.As(err, &conflict) || conflict.CurrentRevision != 1 {
+ t.Fatalf("stale client must conflict: %v", err)
+ }
+ if len(first.Entries) != 2 || first.Entries[0].Content != "" {
+ t.Fatal("conflict modified original snapshot")
+ }
+}
+
+func TestDeltaPreservesUntouchedNotesMetadataAndTombstones(t *testing.T) {
+ cmd := validCommand()
+ cmd.Entries[0].Properties = json.RawMessage(`{"positionIds":["writer"],"source":"user"}`)
+ first, _ := plan(Snapshot{VaultID: cmd.VaultID}, cmd)
+ noteID := cmd.Entries[0].NoteID
+ cmd.BaseRevision = 1
+ cmd.Entries = []Mutation{{NoteID: noteID, Path: "知识/改名.md", Content: "第二版"},
+ {NoteID: uuid.New(), Path: "共享 SOP.md", Content: "流程"}}
+ second, err := plan(first, cmd)
+ if err != nil || len(second.Entries) != 2 {
+ t.Fatal(err)
+ }
+ for _, entry := range second.Entries {
+ if entry.NoteID == noteID && !strings.Contains(string(entry.Properties), "writer") {
+ t.Fatal("omitted properties erased metadata")
+ }
+ }
+ cmd.BaseRevision = 2
+ cmd.Entries = []Mutation{{NoteID: noteID, Path: "知识/改名.md", Deleted: true}}
+ third, err := plan(second, cmd)
+ if err != nil || len(third.Entries) != 2 {
+ t.Fatal("deletion must retain tombstone and unrelated note", err)
+ }
+ for _, entry := range third.Entries {
+ if entry.NoteID == noteID && (!entry.Deleted || entry.Content != "" || entry.Revision != 3) {
+ t.Fatal("invalid tombstone")
+ }
+ }
+ if first.Entries[0].Path != "知识/空白笔记.md" || first.Revision != 1 {
+ t.Fatal("later revisions mutated previous snapshot")
+ }
+}
+
+func TestPathsDuplicatesAndUntrustedPropertiesAreValidated(t *testing.T) {
+ for _, name := range []string{"../secret.md", "/absolute.md", "a\\b.md", "a/../b.md", "a//b.md", "a\x00.md", "C:notes.md", "dir/a ", "dir/a.", "a?.md"} {
+ if ValidatePath(name) == nil {
+ t.Fatalf("unsafe path accepted: %q", name)
+ }
+ }
+ if err := ValidatePath("客户 ACME/项目 決策.md"); err != nil {
+ t.Fatal(err)
+ }
+ for _, properties := range []string{`null`, `[]`, `{"source":"\u0000"}`, `{"source":true} trailing`} {
+ cmd := validCommand()
+ cmd.Entries[0].Properties = json.RawMessage(properties)
+ if _, err := NormalizeCommand(cmd); err == nil {
+ t.Fatalf("invalid metadata accepted: %s", properties)
+ }
+ }
+ cmd := validCommand()
+ cmd.Entries[0].Properties = json.RawMessage(`{"tools":["write"],"authority":"admin"}`)
+ normalized, err := NormalizeCommand(cmd)
+ if err != nil || normalized.ActorUserID != cmd.ActorUserID || normalized.WorkspaceID != cmd.WorkspaceID {
+ t.Fatal("metadata changed authorization", err)
+ }
+ cmd.Entries = append(cmd.Entries, cmd.Entries[0])
+ if _, err := NormalizeCommand(cmd); !errors.Is(err, ErrInvalid) {
+ t.Fatal("duplicate note ID accepted")
+ }
+ cmd = validCommand()
+ cmd.Entries[0].Content = "secret"
+ cmd.Entries[0].Deleted = true
+ if _, err := NormalizeCommand(cmd); !errors.Is(err, ErrInvalid) {
+ t.Fatal("tombstone retained content")
+ }
+}
+
+func TestActivePathsConflictCaseInsensitivelyAndSwapsAreValid(t *testing.T) {
+ cmd := validCommand()
+ cmd.Entries = []Mutation{{NoteID: uuid.New(), Path: "A.md"}, {NoteID: uuid.New(), Path: "a.md"}}
+ if _, err := plan(Snapshot{VaultID: cmd.VaultID}, cmd); !errors.Is(err, ErrPathConflict) {
+ t.Fatal("Windows path collision accepted")
+ }
+ cmd.Entries[1].Path = "B.md"
+ first, err := plan(Snapshot{VaultID: cmd.VaultID}, cmd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cmd.BaseRevision = 1
+ cmd.Entries[0].Path = "B.md"
+ cmd.Entries[1].Path = "A.md"
+ if _, err := plan(first, cmd); err != nil {
+ t.Fatal("atomic path swap rejected", err)
+ }
+}
+
+func TestBoundsAndTitleOnlyCAS(t *testing.T) {
+ cmd := validCommand()
+ cmd.Entries[0].Content = strings.Repeat("x", MaxFileBytes+1)
+ if _, err := NormalizeCommand(cmd); !errors.Is(err, ErrLimit) {
+ t.Fatal("oversize note accepted")
+ }
+ cmd = validCommand()
+ cmd.Entries = nil
+ if _, err := NormalizeCommand(cmd); err == nil {
+ t.Fatal("empty mutation accepted")
+ }
+ title := "岗位知识库"
+ cmd.Title = &title
+ cmd, err := NormalizeCommand(cmd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ result, err := plan(Snapshot{VaultID: cmd.VaultID}, cmd)
+ if err != nil || result.Title != title || result.Revision != 1 || len(result.Entries) != 0 {
+ t.Fatal("title-only commit failed", err)
+ }
+ large := Snapshot{VaultID: cmd.VaultID}
+ for i := 0; i < MaxEntries; i++ {
+ large.Entries = append(large.Entries, Entry{NoteID: uuid.New(), Path: "old.md", Deleted: true, Properties: json.RawMessage(`{}`)})
+ }
+ cmd.Entries = []Mutation{{NoteID: uuid.New(), Path: "new.md"}}
+ if _, err := plan(large, cmd); !errors.Is(err, ErrLimit) {
+ t.Fatal("tombstones were excluded from note-count bound")
+ }
+}
diff --git a/web/package-lock.json b/web/package-lock.json
index ce35cc0..5eec4fe 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -18,16 +18,17 @@
"react-dom": "^19.2.8",
"react-router": "^8.3.0",
"sonner": "^1.7.0",
- "tailwind-merge": "^2.5.4"
+ "tailwind-merge": "^3.7.0"
},
"devDependencies": {
+ "@tailwindcss/postcss": "^4.3.3",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
- "@typescript-eslint/eslint-plugin": "^7.18.0",
- "@typescript-eslint/parser": "^7.18.0",
+ "@typescript-eslint/eslint-plugin": "^8.71.1",
+ "@typescript-eslint/parser": "^8.71.1",
"@vitejs/plugin-react": "^6.0.4",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
@@ -38,7 +39,7 @@
"playwright": "^1.60.0",
"postcss": "^8.5.23",
"prettier": "^3.3.3",
- "tailwindcss": "^3.4.14",
+ "tailwindcss": "^4.3.3",
"typescript": "^5.6.3",
"vite": "^8.1.5",
"vitest": "^4.1.10"
@@ -312,7 +313,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"@emnapi/wasi-threads": "2.0.1",
"tslib": "^2.4.0"
@@ -325,7 +325,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -337,7 +336,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -396,9 +394,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -502,9 +500,9 @@
}
},
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -645,6 +643,17 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -1724,159 +1733,691 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@tanstack/query-core": {
- "version": "5.100.10",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz",
- "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==",
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
+ "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
+ "dev": true,
"license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.24.1",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.3"
}
},
- "node_modules/@tanstack/react-query": {
- "version": "5.100.10",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz",
- "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==",
- "license": "MIT",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
"dependencies": {
- "@tanstack/query-core": "5.100.10"
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
},
"funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
},
- "peerDependencies": {
- "react": "^18 || ^19"
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@testing-library/dom": {
- "version": "10.4.1",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
- "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/code-frame": "^7.10.4",
- "@babel/runtime": "^7.12.5",
- "@types/aria-query": "^5.0.1",
- "aria-query": "5.3.0",
- "dom-accessibility-api": "^0.5.9",
- "lz-string": "^1.5.0",
- "picocolors": "1.1.1",
- "pretty-format": "^27.0.2"
- },
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
"engines": {
- "node": ">=18"
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@testing-library/jest-dom": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz",
- "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@adobe/css-tools": "^4.4.0",
- "aria-query": "^5.0.0",
- "css.escape": "^1.5.1",
- "dom-accessibility-api": "^0.6.3",
- "picocolors": "^1.1.1",
- "redent": "^3.0.0"
- },
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
"engines": {
- "node": ">=22",
- "npm": ">=6",
- "yarn": ">=1"
+ "node": ">= 12.0.0"
},
- "peerDependencies": {
- "@testing-library/dom": ">=10 <11"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
- "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "license": "MIT"
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
},
- "node_modules/@testing-library/react": {
- "version": "16.3.2",
- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
- "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.12.5"
- },
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@testing-library/dom": "^10.0.0",
- "@types/react": "^18.0.0 || ^19.0.0",
- "@types/react-dom": "^18.0.0 || ^19.0.0",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
+ "node": ">= 12.0.0"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@testing-library/user-event": {
- "version": "14.6.3",
- "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz",
- "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "license": "MIT",
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=12",
- "npm": ">=6"
+ "node": ">= 12.0.0"
},
- "peerDependencies": {
- "@testing-library/dom": ">=7.21.4"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@tybys/wasm-util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
- "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "license": "MIT",
+ "license": "MPL-2.0",
"optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@types/aria-query": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
- "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
},
- "node_modules/@types/chai": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/deep-eql": "*",
- "assertion-error": "^2.0.1"
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
+ "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-x64": "4.3.3",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.3",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.3",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.3",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
+ "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
+ "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
+ "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
+ "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
+ "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
+ "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
+ "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
+ "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
+ "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
+ "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
+ "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
+ "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz",
+ "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.3.3",
+ "@tailwindcss/oxide": "4.3.3",
+ "postcss": "^8.5.16",
+ "tailwindcss": "4.3.3"
+ }
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.100.10",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz",
+ "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.100.10",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz",
+ "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.100.10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz",
+ "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=22",
+ "npm": ">=6",
+ "yarn": ">=1"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=10 <11"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz",
+ "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
},
@@ -1935,122 +2476,159 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
- "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.1.tgz",
+ "integrity": "sha512-nNlBf6HuotqMgZvSkWD5hNdemiBIxQ2NmrlgfcUIeZRs+BbakjscCQRq/Hb74EFVnFiSWWdL12JruqwBy3ERDw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "7.18.0",
- "@typescript-eslint/type-utils": "7.18.0",
- "@typescript-eslint/utils": "7.18.0",
- "@typescript-eslint/visitor-keys": "7.18.0",
- "graphemer": "^1.4.0",
- "ignore": "^5.3.1",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.71.1",
+ "@typescript-eslint/type-utils": "8.71.1",
+ "@typescript-eslint/utils": "8.71.1",
+ "@typescript-eslint/visitor-keys": "8.71.1",
+ "ignore": "^7.0.5",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^1.3.0"
+ "ts-api-utils": "^2.5.0"
},
"engines": {
- "node": "^18.18.0 || >=20.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^7.0.0",
- "eslint": "^8.56.0"
+ "@typescript-eslint/parser": "^8.71.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz",
+ "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.1.tgz",
+ "integrity": "sha512-oM/5sAqz/l1v/zYK1qTOPrn2+JIrBGC1V6uNYucRO5gh6eT9vWJ/RhPbGE59byvZEUQ4VP6XkLBaS5Jti5jpvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.71.1",
+ "@typescript-eslint/types": "8.71.1",
+ "@typescript-eslint/typescript-estree": "8.71.1",
+ "@typescript-eslint/visitor-keys": "8.71.1",
+ "debug": "^4.4.3"
},
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@typescript-eslint/parser": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
- "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.1.tgz",
+ "integrity": "sha512-Oijc9RUsohHjncg6iGi7gCtjnEjYt9FWv9u6ygK8uKcHK5eLjLAkqERQ9BaytbpceOFdk6kifPCS/zAZE+A99g==",
"dev": true,
- "license": "BSD-2-Clause",
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "7.18.0",
- "@typescript-eslint/types": "7.18.0",
- "@typescript-eslint/typescript-estree": "7.18.0",
- "@typescript-eslint/visitor-keys": "7.18.0",
- "debug": "^4.3.4"
+ "@typescript-eslint/tsconfig-utils": "^8.71.1",
+ "@typescript-eslint/types": "^8.71.1",
+ "debug": "^4.4.3"
},
"engines": {
- "node": "^18.18.0 || >=20.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.56.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
- "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.1.tgz",
+ "integrity": "sha512-1YUDZXdTnXLlob03SFHBipdyMBsDVm7zoSz0ytTJ39dk5J1TgsTK03VlR/dLXG/RLLUrPcYbD4rp23BmHxHP0w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "7.18.0",
- "@typescript-eslint/visitor-keys": "7.18.0"
+ "@typescript-eslint/types": "8.71.1",
+ "@typescript-eslint/visitor-keys": "8.71.1"
},
"engines": {
- "node": "^18.18.0 || >=20.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.1.tgz",
+ "integrity": "sha512-oTvkml5SxXhgg+WWKhod5qWQICDXtOB+/Hi9VdPKFAANJ2b7YOgMffgvzXuqog7R6A/JtrzRf128dsxummyWhg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
- "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.1.tgz",
+ "integrity": "sha512-kH3t3jZCYv2PZSBEWPKTariLrCaoF1CLoPitYFBJOhAGEwWnSRAtGEifrfrpBa4K9JlwWNZotgA5w3/wLSk/FQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "7.18.0",
- "@typescript-eslint/utils": "7.18.0",
- "debug": "^4.3.4",
- "ts-api-utils": "^1.3.0"
+ "@typescript-eslint/types": "8.71.1",
+ "@typescript-eslint/typescript-estree": "8.71.1",
+ "@typescript-eslint/utils": "8.71.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
},
"engines": {
- "node": "^18.18.0 || >=20.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.56.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/types": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
- "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.1.tgz",
+ "integrity": "sha512-ybYnPTUwg3VQQOFEiBPLs8Jxl1ry2vXdn1kVKguomBbvxpetdu3MKTNDqFNUrlG8ylP9MpZosmXMFYlGQHk1oQ==",
"dev": true,
"license": "MIT",
"engines": {
- "node": "^18.18.0 || >=20.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
@@ -2058,75 +2636,88 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
- "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.1.tgz",
+ "integrity": "sha512-pSKSEK1JJpHs6KiKVtABCE4iJK5d7FLFzDXQNBQ2oRY7LEd3UwOt3TZemTgu9syTIf56dJWlTn/pdmrO9oKUXQ==",
"dev": true,
- "license": "BSD-2-Clause",
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "7.18.0",
- "@typescript-eslint/visitor-keys": "7.18.0",
- "debug": "^4.3.4",
- "globby": "^11.1.0",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^1.3.0"
+ "@typescript-eslint/project-service": "8.71.1",
+ "@typescript-eslint/tsconfig-utils": "8.71.1",
+ "@typescript-eslint/types": "8.71.1",
+ "@typescript-eslint/visitor-keys": "8.71.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
},
"engines": {
- "node": "^18.18.0 || >=20.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
- "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.1.tgz",
+ "integrity": "sha512-CBjT6gfAz3DW2j9Q4moFnqt208Aq+506sVGXfpRcyAeqn0EpayL0mjdWoCO2t3RC7GewNrFxdeehuEaPg6DMzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "7.18.0",
- "@typescript-eslint/types": "7.18.0",
- "@typescript-eslint/typescript-estree": "7.18.0"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.71.1",
+ "@typescript-eslint/types": "8.71.1",
+ "@typescript-eslint/typescript-estree": "8.71.1"
},
"engines": {
- "node": "^18.18.0 || >=20.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.56.0"
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
- "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
+ "version": "8.71.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.1.tgz",
+ "integrity": "sha512-0GxiUGqMU0qXJt58cBsiG3DP8lUmBaFo2vChtUX+CT7PnR3v/VGLLrzVP4K9r1lNiZXjHYefMBbPunplr09v6w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "7.18.0",
- "eslint-visitor-keys": "^3.4.3"
+ "@typescript-eslint/types": "8.71.1",
+ "eslint-visitor-keys": "^5.0.0"
},
"engines": {
- "node": "^18.18.0 || >=20.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
@@ -2339,34 +2930,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/any-promise": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
- "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/arg": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
- "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2396,16 +2959,6 @@
"dequal": "^2.0.3"
}
},
- "node_modules/array-union": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
- "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -2483,40 +3036,27 @@
"require-from-string": "^2.0.2"
}
},
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/brace-expansion": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
- "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0"
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
}
},
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "node_modules/brace-expansion/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
"engines": {
- "node": ">=8"
+ "node": "18 || 20 || >=22"
}
},
"node_modules/browserslist": {
@@ -2563,16 +3103,6 @@
"node": ">=6"
}
},
- "node_modules/camelcase-css": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
- "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/caniuse-lite": {
"version": "1.0.30001810",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
@@ -2621,44 +3151,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/chokidar/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/cli-width": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
@@ -2713,16 +3205,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/commander": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
- "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2793,19 +3275,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "cssesc": "bin/cssesc"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -2885,33 +3354,6 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
- "node_modules/didyoumean": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
- "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/dir-glob": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
- "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-type": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/dlv": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
- "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -2947,6 +3389,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/enhanced-resolve": {
+ "version": "5.26.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz",
+ "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/entities": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
@@ -2960,16 +3416,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/es-module-lexer": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
@@ -3111,9 +3557,9 @@
}
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3225,36 +3671,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fast-glob": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
- "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.8"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -3319,19 +3735,6 @@
"node": "^10.12.0 || >=12.0.0"
}
},
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -3407,16 +3810,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -3472,9 +3865,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
+ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3511,26 +3904,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/globby": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
- "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "array-union": "^2.1.0",
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.9",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
+ "license": "ISC"
},
"node_modules/graphemer": {
"version": "1.4.0",
@@ -3559,19 +3938,6 @@
"node": ">=8"
}
},
- "node_modules/hasown": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
- "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/headers-polyfill": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz",
@@ -3662,35 +4028,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-core-module": {
- "version": "2.16.2",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
- "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "hasown": "^2.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3731,16 +4068,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
"node_modules/is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
@@ -3766,13 +4093,13 @@
"license": "ISC"
},
"node_modules/jiti": {
- "version": "1.21.7",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
- "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"license": "MIT",
"bin": {
- "jiti": "bin/jiti.js"
+ "jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": {
@@ -4153,26 +4480,6 @@
"url": "https://opencollective.com/parcel"
}
},
- "node_modules/lilconfig": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
- "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/antonk52"
- }
- },
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -4233,40 +4540,16 @@
"dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/mdn-data": {
- "version": "2.27.1",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
- "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
- "dev": true,
- "license": "CC0-1.0"
- },
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/micromatch": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
+ "@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -4278,16 +4561,16 @@
}
},
"node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^2.0.2"
+ "brace-expansion": "^5.0.8"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -4371,18 +4654,6 @@
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/mz": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
- "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "any-promise": "^1.0.0",
- "object-assign": "^4.0.1",
- "thenify-all": "^1.0.0"
- }
- },
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
@@ -4419,36 +4690,6 @@
"node": ">=18"
}
},
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-hash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
- "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/obug": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
@@ -4586,13 +4827,6 @@
"node": ">=8"
}
},
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
@@ -4600,16 +4834,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -4624,39 +4848,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/picomatch": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/pirates": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
- "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
@@ -4733,133 +4924,6 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/postcss-import": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
- "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "postcss-value-parser": "^4.0.0",
- "read-cache": "^1.0.0",
- "resolve": "^1.1.7"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "postcss": "^8.0.0"
- }
- },
- "node_modules/postcss-js": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
- "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "camelcase-css": "^2.0.1"
- },
- "engines": {
- "node": "^12 || ^14 || >= 16"
- },
- "peerDependencies": {
- "postcss": "^8.4.21"
- }
- },
- "node_modules/postcss-load-config": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
- "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "lilconfig": "^3.1.1"
- },
- "engines": {
- "node": ">= 18"
- },
- "peerDependencies": {
- "jiti": ">=1.21.0",
- "postcss": ">=8.0.9",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- },
- "postcss": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/postcss-nested": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
- "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "postcss-selector-parser": "^6.1.1"
- },
- "engines": {
- "node": ">=12.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.14"
- }
- },
- "node_modules/postcss-selector-parser": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
- "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@@ -5073,29 +5137,6 @@
}
}
},
- "node_modules/read-cache": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
- "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "pify": "^2.3.0"
- }
- },
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -5130,28 +5171,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/resolve": {
- "version": "1.22.12",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
- "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "is-core-module": "^2.16.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -5275,9 +5294,9 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
- "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -5337,16 +5356,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/sonner": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
@@ -5358,9 +5367,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -5452,29 +5461,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/sucrase": {
- "version": "3.35.1",
- "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
- "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.2",
- "commander": "^4.0.0",
- "lines-and-columns": "^1.1.6",
- "mz": "^2.7.0",
- "pirates": "^4.0.1",
- "tinyglobby": "^0.2.11",
- "ts-interface-checker": "^0.1.9"
- },
- "bin": {
- "sucrase": "bin/sucrase",
- "sucrase-node": "bin/sucrase-node"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -5488,19 +5474,6 @@
"node": ">=8"
}
},
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -5522,9 +5495,9 @@
}
},
"node_modules/tailwind-merge": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz",
- "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==",
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz",
+ "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -5532,41 +5505,24 @@
}
},
"node_modules/tailwindcss": {
- "version": "3.4.19",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
- "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
+ "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@alloc/quick-lru": "^5.2.0",
- "arg": "^5.0.2",
- "chokidar": "^3.6.0",
- "didyoumean": "^1.2.2",
- "dlv": "^1.1.3",
- "fast-glob": "^3.3.2",
- "glob-parent": "^6.0.2",
- "is-glob": "^4.0.3",
- "jiti": "^1.21.7",
- "lilconfig": "^3.1.3",
- "micromatch": "^4.0.8",
- "normalize-path": "^3.0.0",
- "object-hash": "^3.0.0",
- "picocolors": "^1.1.1",
- "postcss": "^8.4.47",
- "postcss-import": "^15.1.0",
- "postcss-js": "^4.0.1",
- "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
- "postcss-nested": "^6.2.0",
- "postcss-selector-parser": "^6.1.2",
- "resolve": "^1.22.8",
- "sucrase": "^3.35.0"
- },
- "bin": {
- "tailwind": "lib/cli.js",
- "tailwindcss": "lib/cli.js"
- },
"engines": {
- "node": ">=14.0.0"
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
}
},
"node_modules/text-table": {
@@ -5576,29 +5532,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/thenify": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
- "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "any-promise": "^1.0.0"
- }
- },
- "node_modules/thenify-all": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
- "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "thenify": ">= 3.1.0 < 4"
- },
- "engines": {
- "node": ">=0.8"
- }
- },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -5694,19 +5627,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
"node_modules/tough-cookie": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
@@ -5734,25 +5654,18 @@
}
},
"node_modules/ts-api-utils": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
- "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=16"
+ "node": ">=18.12"
},
"peerDependencies": {
- "typescript": ">=4.2.0"
+ "typescript": ">=4.8.4"
}
},
- "node_modules/ts-interface-checker": {
- "version": "0.1.13",
- "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
- "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true,
- "license": "Apache-2.0"
- },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -5800,9 +5713,9 @@
}
},
"node_modules/undici": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
- "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "version": "7.30.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
+ "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5910,13 +5823,6 @@
}
}
},
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/vite": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
diff --git a/web/package.json b/web/package.json
index 2988971..177f4d8 100644
--- a/web/package.json
+++ b/web/package.json
@@ -37,16 +37,17 @@
"react-dom": "^19.2.8",
"react-router": "^8.3.0",
"sonner": "^1.7.0",
- "tailwind-merge": "^2.5.4"
+ "tailwind-merge": "^3.7.0"
},
"devDependencies": {
+ "@tailwindcss/postcss": "^4.3.3",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
- "@typescript-eslint/eslint-plugin": "^7.18.0",
- "@typescript-eslint/parser": "^7.18.0",
+ "@typescript-eslint/eslint-plugin": "^8.71.1",
+ "@typescript-eslint/parser": "^8.71.1",
"@vitejs/plugin-react": "^6.0.4",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
@@ -57,7 +58,7 @@
"playwright": "^1.60.0",
"postcss": "^8.5.23",
"prettier": "^3.3.3",
- "tailwindcss": "^3.4.14",
+ "tailwindcss": "^4.3.3",
"typescript": "^5.6.3",
"vite": "^8.1.5",
"vitest": "^4.1.10"
diff --git a/web/postcss.config.js b/web/postcss.config.js
index 2aa7205..51a6e4e 100644
--- a/web/postcss.config.js
+++ b/web/postcss.config.js
@@ -1,6 +1,6 @@
export default {
plugins: {
- tailwindcss: {},
+ '@tailwindcss/postcss': {},
autoprefixer: {},
},
};
diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css
index 41ef4dd..3e10e37 100644
--- a/web/src/styles/globals.css
+++ b/web/src/styles/globals.css
@@ -1,8 +1,14 @@
@import './design-tokens.generated.css';
+@import 'tailwindcss';
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
+@config '../../tailwind.config.js';
+
+/* Preserve the existing v3 utility scale while removing its unpatched
+ * dependency chain. The product palette remains in the pinned JS config. */
+@theme {
+ --radius-sm: 0.125rem;
+ --blur-sm: 4px;
+}
@layer base {
html,
@@ -11,6 +17,16 @@
height: 100%;
}
+ button:not(:disabled),
+ [role='button']:not(:disabled) {
+ cursor: pointer;
+ }
+
+ input::placeholder,
+ textarea::placeholder {
+ color: rgb(156 163 175);
+ }
+
body {
@apply bg-bg text-fg font-sans antialiased;
font-size: 14px;
@@ -48,7 +64,7 @@
/* Focus ring — keyboard only, geometric not glow-y */
:focus-visible {
- @apply outline-none ring-2 ring-accent/60 ring-offset-2 ring-offset-bg;
+ @apply outline-hidden ring-2 ring-accent/60 ring-offset-2 ring-offset-bg;
}
/* Heading defaults */
@@ -108,3 +124,13 @@
background: linear-gradient(90deg, transparent, rgb(var(--border-strong) / 0.25), transparent);
animation: shimmer 1.6s infinite;
}
+
+/* Keep existing class consumers' keyboard outline visible in forced colors.
+ * These declarations follow the v4 utility layer's outline-style: none. */
+@layer utilities {
+ .outline-none,
+ .focus-visible\:outline-none:focus-visible {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ }
+}
diff --git a/web/theme-acceptance.mjs b/web/theme-acceptance.mjs
index a347a50..e2f0bfe 100644
--- a/web/theme-acceptance.mjs
+++ b/web/theme-acceptance.mjs
@@ -116,6 +116,38 @@ async function assertTheme(page, theme) {
assert.equal(await page.locator('[data-toast-theme]').getAttribute('data-toast-theme'), theme);
}
+async function assertUtilityCompatibility(page) {
+ await page.keyboard.press('Tab');
+ const values = await page.evaluate(() => {
+ const probe = document.createElement('button');
+ probe.className = 'rounded-sm backdrop-blur-sm text-fg bg-accent/10 outline-none';
+ const focusProbe = document.createElement('button');
+ focusProbe.className = 'focus-visible:outline-none';
+ const reference = document.createElement('span');
+ reference.style.color = 'rgb(var(--fg) / calc(var(--fg-opacity, 1) * 1))';
+ reference.style.backgroundColor = 'rgb(var(--accent) / calc(var(--accent-opacity, 1) * 0.1))';
+ document.body.append(probe, focusProbe, reference); focusProbe.focus();
+ const rgba = (value) => {
+ const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1;
+ const context = canvas.getContext('2d'); context.fillStyle = value; context.fillRect(0, 0, 1, 1);
+ return [...context.getImageData(0, 0, 1, 1).data];
+ };
+ const style = getComputedStyle(probe); const focused = getComputedStyle(focusProbe); const expected = getComputedStyle(reference);
+ const result = { radius: style.borderRadius, blur: style.backdropFilter, cursor: style.cursor,
+ outlineStyle: style.outlineStyle, outlineWidth: style.outlineWidth,
+ focusVisible: focusProbe.matches(':focus-visible'), focusOutlineStyle: focused.outlineStyle, focusOutlineWidth: focused.outlineWidth,
+ foreground: rgba(style.color), expectedForeground: rgba(expected.color),
+ accent: rgba(style.backgroundColor), expectedAccent: rgba(expected.backgroundColor) };
+ probe.remove(); focusProbe.remove(); reference.remove(); return result;
+ });
+ assert.equal(values.radius, '2px'); assert.equal(values.blur, 'blur(4px)'); assert.equal(values.cursor, 'pointer');
+ assert.equal(values.outlineStyle, 'solid'); assert.equal(values.outlineWidth, '2px');
+ assert.equal(values.focusVisible, true); assert.equal(values.focusOutlineStyle, 'solid'); assert.equal(values.focusOutlineWidth, '2px');
+ for (const [actual, expected] of [[values.foreground, values.expectedForeground], [values.accent, values.expectedAccent]]) {
+ assert.ok(actual.every((value, index) => Math.abs(value - expected[index]) <= 1), 'theme color/opacity utility changed');
+ }
+}
+
const vite = startVite();
let browser;
let failure;
@@ -135,6 +167,7 @@ try {
const lightToggle = defaultPage.getByRole('button', { name: 'Switch to light theme' });
await lightToggle.waitFor();
await assertTheme(defaultPage, 'dark');
+ await assertUtilityCompatibility(defaultPage);
assert.equal(
await defaultPage.evaluate(() => localStorage.getItem('mem.theme')),
null,
@@ -148,6 +181,10 @@ try {
await lightToggle.click();
await assertTheme(defaultPage, 'light');
+ await assertUtilityCompatibility(defaultPage);
+ await defaultPage.emulateMedia({ forcedColors: 'active' });
+ await assertUtilityCompatibility(defaultPage);
+ await defaultPage.emulateMedia({ forcedColors: 'none' });
assert.equal(await defaultPage.evaluate(() => localStorage.getItem('mem.theme')), 'light');
assert.equal(
(await defaultPage.evaluate(() => getComputedStyle(document.body).backgroundColor)).replaceAll(
From 9a618cb59c04e299ddb947b6a5a387fcbf5a346d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 10 Oct 2026 12:48:44 +0800
Subject: [PATCH 3/6] build(deps): bump pypdf from 6.18.0 to 6.19.0 in /worker
(#236)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.18.0 to 6.19.0.
Release notes
Sourced from pypdf's
releases.
Version 6.19.0, 2026-09-16
What's new
Security (SEC)
Deprecations (DEP)
Performance Improvements (PI)
Bug Fixes (BUG)
Full
Changelog
Version 6.18.1, 2026-09-11
What's new
Security (SEC)
Bug Fixes (BUG)
Robustness (ROB)
Documentation (DOC)
Full
Changelog
Changelog
Sourced from pypdf's
changelog.
Version 6.19.0, 2026-09-16
Security (SEC)
- Limit size of alphabetical page labels (#4096)
Deprecations (DEP)
- Replace PdfWriter method add_js (#3979)
Performance Improvements (PI)
- Move static value out of loop body for appearance stream data (#4087)
- Reduce number of full data lookups for attachment mapping API (#4081)
Bug Fixes (BUG)
- Do not copy unrelated pages when appending pages with non-terminal
fields (#4078)
- Use page reference for existing internal link targets (#4076)
- Arabic-Indic digits are reversed during text extraction (#4077)
- Parse a string rect for add_uri into a rectangle (#4074)
Full
Changelog
Version 6.18.1, 2026-09-11
Security (SEC)
- Further restrict FlateDecode recovery (#4073)
- Limit entry count for TrueType and Type1 font
/Widths
(#4072)
- Limit allowed length of tokens in parse_bfchar (#4071)
Bug Fixes (BUG)
- Use current text matrix for visitor_text (#4062)
- Repeat the letter for /S /A and /S /a page labels past Z (#4065)
- Use font color for FreeText default appearance (#4051)
Robustness (ROB)
- Fix compatibility with fonttools < 4.58.0 (#4050,
#4059)
Documentation (DOC)
- Use combined matrix in visitor examples (#4066)
Full
Changelog
Commits
d62cb58
REL: 6.19.0
0d8b5a8
SEC: Limit size of alphabetical page labels (#4096)
959467a
PI: Move static value out of loop body for appearance stream data (#4087)
20822b2
DEV: Bump zizmorcore/zizmor-action from 0.6.3 to 0.6.4 (#4093)
25f2301
DEP: Replace PdfWriter method add_js (#3979)
a924438
BUG: Do not copy unrelated pages when appending pages with non-terminal
field...
cf5cec2
BUG: Use page reference for existing internal link targets (#4076)
96d81f0
BUG: Arabic-Indic digits are reversed during text extraction (#4077)
2d21901
DEV: Fix Color class for latest mypy (#4082)
893a010
MAINT: Split PdfDocCommon._flatten (#4070)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/bytefolk/mem/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 修雨 <47820304+PeterGuy326@users.noreply.github.com>
---
worker/pyproject.toml | 2 +-
worker/uv.lock | 80 +++++++++++++++++++++----------------------
2 files changed, 41 insertions(+), 41 deletions(-)
diff --git a/worker/pyproject.toml b/worker/pyproject.toml
index b965758..76fe1f4 100644
--- a/worker/pyproject.toml
+++ b/worker/pyproject.toml
@@ -28,7 +28,7 @@ dependencies = [
# Imaging
"pillow>=10.2",
# PDF (placeholder — W2 actually uses)
- "pypdf>=6.18.0",
+ "pypdf>=6.19.0",
# HTTP
"requests>=2.31",
"httpx>=0.27",
diff --git a/worker/uv.lock b/worker/uv.lock
index 482ddc8..0bf50a4 100644
--- a/worker/uv.lock
+++ b/worker/uv.lock
@@ -3,12 +3,12 @@ revision = 3
requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.15' and sys_platform != 'darwin'",
- "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
- "python_full_version == '3.12.*' and sys_platform != 'darwin'",
- "python_full_version < '3.12' and sys_platform != 'darwin'",
"python_full_version >= '3.15' and sys_platform == 'darwin'",
+ "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
"python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform == 'darwin'",
+ "python_full_version == '3.12.*' and sys_platform != 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'darwin'",
+ "python_full_version < '3.12' and sys_platform != 'darwin'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
]
@@ -986,7 +986,7 @@ requires-dist = [
{ name = "protobuf", specifier = ">=7.35.1" },
{ name = "pydantic", specifier = ">=2.6" },
{ name = "pydantic-settings", specifier = ">=2.2" },
- { name = "pypdf", specifier = ">=6.18.0" },
+ { name = "pypdf", specifier = ">=6.19.0" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.0" },
{ name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.23" },
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=6.0" },
@@ -1210,10 +1210,10 @@ version = "2.5.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15' and sys_platform != 'darwin'",
- "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
- "python_full_version == '3.12.*' and sys_platform != 'darwin'",
"python_full_version >= '3.15' and sys_platform == 'darwin'",
+ "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
"python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform == 'darwin'",
+ "python_full_version == '3.12.*' and sys_platform != 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'darwin'",
]
sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" }
@@ -1644,11 +1644,11 @@ wheels = [
[[package]]
name = "pypdf"
-version = "6.18.0"
+version = "6.19.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c2/c3/9fa0666f280552bd3833562d985b785fce1ddb3804937edd2fd6a3f2bdb3/pypdf-6.18.0.tar.gz", hash = "sha256:ae58b7d93c22c169ffb02c3b06321c45c4f223b4916536568adb57d789d95d01", size = 7024871, upload-time = "2026-09-07T16:48:16.444Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/ac/63d71aaedb59acbcdef491e6ca6469165e3771c9c74358204818fd9bc5a6/pypdf-6.19.0.tar.gz", hash = "sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155", size = 7033266, upload-time = "2026-09-16T09:32:05.946Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/a5/d5922a078c9a612327681d4793404f82998f4d66213add6fdc0659245856/pypdf-6.18.0-py3-none-any.whl", hash = "sha256:05b762b77bcb9dcb4a7c91fcf5dded585b25bee7269ab3d3001d7c55fa1b324b", size = 393848, upload-time = "2026-09-07T16:48:14.254Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl", hash = "sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14", size = 395480, upload-time = "2026-09-16T09:32:04.087Z" },
]
[[package]]
@@ -2040,7 +2040,7 @@ resolution-markers = [
"python_full_version < '3.12' and sys_platform == 'darwin'",
]
dependencies = [
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [
@@ -2112,14 +2112,14 @@ version = "1.18.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15' and sys_platform != 'darwin'",
- "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
- "python_full_version == '3.12.*' and sys_platform != 'darwin'",
"python_full_version >= '3.15' and sys_platform == 'darwin'",
+ "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
"python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform == 'darwin'",
+ "python_full_version == '3.12.*' and sys_platform != 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'darwin'",
]
dependencies = [
- { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" }
wheels = [
@@ -2213,7 +2213,7 @@ resolution-markers = [
"python_full_version < '3.12' and sys_platform == 'darwin'",
]
dependencies = [
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" }
wheels = [
@@ -2226,14 +2226,14 @@ version = "2026.7.31"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15' and sys_platform != 'darwin'",
- "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
- "python_full_version == '3.12.*' and sys_platform != 'darwin'",
"python_full_version >= '3.15' and sys_platform == 'darwin'",
+ "python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
"python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform == 'darwin'",
+ "python_full_version == '3.12.*' and sys_platform != 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'darwin'",
]
dependencies = [
- { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/21/62/083288b8d6b9ecb2968e7573e60aa0694089e960e203934bc217169acf64/tifffile-2026.7.31.tar.gz", hash = "sha256:79b1f4b1aba3ef3e6b6f1691a32abb62f5d7383faa52a12771c695232ba40bee", size = 442177, upload-time = "2026-08-01T02:28:32.523Z" }
wheels = [
@@ -2349,13 +2349,13 @@ resolution-markers = [
"python_full_version < '3.12' and sys_platform == 'darwin'",
]
dependencies = [
- { name = "filelock", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" },
- { name = "fsspec", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" },
- { name = "jinja2", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" },
- { name = "networkx", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" },
- { name = "setuptools", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" },
- { name = "sympy", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" },
- { name = "typing-extensions", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "setuptools" },
+ { name = "sympy" },
+ { name = "typing-extensions" },
]
wheels = [
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", upload-time = "2026-07-08T12:26:13Z" },
@@ -2371,19 +2371,19 @@ version = "2.13.0+cpu"
source = { registry = "https://download.pytorch.org/whl/cpu" }
resolution-markers = [
"python_full_version >= '3.15' and sys_platform != 'darwin'",
+ "python_full_version >= '3.15' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
"python_full_version == '3.12.*' and sys_platform != 'darwin'",
"python_full_version < '3.12' and sys_platform != 'darwin'",
- "python_full_version >= '3.15' and sys_platform == 'darwin'",
]
dependencies = [
- { name = "filelock", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" },
- { name = "fsspec", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" },
- { name = "jinja2", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" },
- { name = "networkx", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" },
- { name = "setuptools", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" },
- { name = "sympy", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" },
- { name = "typing-extensions", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "setuptools" },
+ { name = "sympy" },
+ { name = "typing-extensions" },
]
wheels = [
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:6e9817dbdf5ea76789babd46e457eac5bf14ff566cf85f8addbfdff2d56601ce", upload-time = "2026-07-08T19:27:52Z" },
@@ -2425,10 +2425,10 @@ resolution-markers = [
"python_full_version < '3.12' and sys_platform == 'darwin'",
]
dependencies = [
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform == 'darwin'" },
- { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'darwin'" },
- { name = "pillow", marker = "python_full_version < '3.15' and sys_platform == 'darwin'" },
- { name = "torch", version = "2.13.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version < '3.15' and sys_platform == 'darwin'" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.15'" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.15'" },
+ { name = "pillow" },
+ { name = "torch", version = "2.13.0", source = { registry = "https://download.pytorch.org/whl/cpu" } },
]
wheels = [
{ url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:83fe6c020866a85acd7d97deccc45ff11d66daf42916d04396a4309c66c0ccb8", upload-time = "2026-07-08T12:26:40Z" },
@@ -2444,16 +2444,16 @@ version = "0.28.0+cpu"
source = { registry = "https://download.pytorch.org/whl/cpu" }
resolution-markers = [
"python_full_version >= '3.15' and sys_platform != 'darwin'",
+ "python_full_version >= '3.15' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and python_full_version < '3.15' and sys_platform != 'darwin'",
"python_full_version == '3.12.*' and sys_platform != 'darwin'",
"python_full_version < '3.12' and sys_platform != 'darwin'",
- "python_full_version >= '3.15' and sys_platform == 'darwin'",
]
dependencies = [
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform != 'darwin'" },
- { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'darwin') or (python_full_version >= '3.15' and sys_platform == 'darwin')" },
- { name = "pillow", marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" },
- { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version >= '3.15' or sys_platform != 'darwin'" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' or sys_platform == 'darwin'" },
+ { name = "pillow" },
+ { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" } },
]
wheels = [
{ url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.28.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:22958193d72444ed7cbcc665ba4821a31e5279f9c4d1ad08520918b30896b78a", upload-time = "2026-07-08T12:26:39Z" },
From 73c316abd43c40bb5b95c60dd0628219d72fdd57 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 10 Oct 2026 12:56:47 +0800
Subject: [PATCH 4/6] build(deps): bump urllib3 from 2.7.0 to 2.8.0 in /worker
(#233)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.7.0 to 2.8.0.
Release notes
Sourced from urllib3's
releases.
2.8.0
🚀 urllib3 is fundraising for HTTP/2 support
urllib3
is raising ~$40,000 USD to release HTTP/2 support and ensure
long-term sustainable maintenance of the project. If your company or
organization uses Python and would benefit from HTTP/2 support in
Requests, pip, cloud SDKs, and thousands of other projects please consider contributing
financially to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.
Thank you for your support.
Security
Fixed the following security issues:
- The TLS configuration for HTTPS proxies could be ignored or
overridden. (High severity, GHSA-8988-9cw3-xx77)
HTTPResponse.stream() and read_chunked()
could buffer a chunk-size line of unbounded length in memory. (High
severity, GHSA-vxq7-64xx-v4gw)
- Chunked Deflate streaming could enter an infinite loop. (Medium
severity, GHSA-gh4c-6fx4-qh6g)
[!IMPORTANT]
urllib3 2.8.0 fixes HTTPS proxy TLS configuration being ignored or
overridden by destination settings. Configurations relying on that
behavior may require changes.
Configure proxy CA certificates and client certificates in
proxy_ssl_context, and proxy identity checks with
proxy_assert_hostname or
proxy_assert_fingerprint. Destination client certificates
and identity overrides no longer apply to HTTPS forwarding proxy
connections.
[!NOTE]
CVE IDs had not yet been assigned to these advisories at the time of
release due to a backlog at GitHub's CNA.
Deprecations & Removals
- Deprecated using an empty collection as the
Retry
option allowed_methods to retry any verb. (#5044)
Features
- Added
Url.auth_decoded and
Url.auth_decoded_joined convenience properties to the
result of parse_url(). (#4945)
- Added
basic_auth_encoding and
proxy_basic_auth_encoding parameters to
urllib3.util.make_headers(). (#5092)
Bugfixes
-
Fixed response header handling to replace obsolete folded header
lines (obs-fold) with spaces in accordance with RFC 9112,
preventing raw CRLF sequences from appearing in header values such as
Set-Cookie. (#1362)
-
Fixed usage of proxy_ssl_context with
ProxyManager when
use_forwarding_for_https=True. Passing
ssl_context instead of proxy_ssl_context for
HTTPS proxies in this configuration now emits a
FutureWarning and will raise an error in v3.0. (#2577)
-
Changed behavior of the default ConnectionPool.pool
initialization. LifoQueue is now resolved from the
queue module after the ConnectionPool is
instantiated instead of using the default cached QueueCls
class property. This is done because sometimes the
queue.LifoQueue is monkey-patched late in the program, such
as by gevent. (#3289)
-
Raised UnrewindableBodyError instead of
ValueError when retrying a request whose body had
tell() but not seek(). (#3779)
-
Decoded percent-encoded SOCKS proxy credentials before authenticating
with the proxy server. (#3785)
-
Fixed HTTPResponse.drain_conn() to discard unread
response data in 64 KiB chunks (same as the default amt
when doing HTTPResponse.stream(...)). (#5019)
-
Fixed is_ipaddress() to detect non-standard IPv4 forms
accepted by socket.connect, such as hex
(0x7f000001), octal (0177.0.0.1), and decimal
integers (2130706433), ensuring SSL certificate
verification uses the correct mode for these addresses. (#5029)
-
Fixed HTTPConnectionPool.urlopen raising a misleading
FullPoolError instead of ValueError when
called with an invalid timeout argument on a pool created
with block=True. (#5059)
-
Fixed port-zero handling to preserve explicit :0 values
instead of substituting the default ports 80 or 443 in URL parsing, pool
selection, proxy configuration, connection_from_url(), and
HTTP/2 request authority. (#5071,
#5101)
-
Fixed a bug where PoolManager passed the
assert_hostname and assert_fingerprint
parameters to HTTP connection pools. (#5077)
-
Fixed HTTPConnectionPool.urlopen() and HTTP proxy
forwarding to strip URL fragments from absolute request targets before
sending requests. (#5079)
-
Added safeguards to the proxy tunneling code to prevent potential
security issues when handling invalid characters in the proxy host and
HTTP headers. This change affects users of Python 3.10, Python 3.11, and
Python 3.12 when the standard library does not contain the fix; those on
newer Python versions should upgrade to 3.13.14+ or 3.14.5+ to get the
same security fixes. (#5091)
-
Fixed HTTPSConnection.connect() overriding
ProxyConfig.ssl_context's certificate policy and proxy
identity checks with the target connection's TLS settings when
forwarding through an HTTPS proxy.
HTTPSConnection no longer applies target SNI,
assertions, or client credentials to forwarding proxy handshakes and
continues to use its ssl_context as a fallback when an
HTTPS proxy forwards an HTTP target. (#5093)
-
Fixed URL parsing to more strictly enforce RFC 3986 host syntax,
rejecting invalid host input such as raw spaces and control characters,
malformed percent-encodings, and percent-encoded control characters in
HTTP(S) hosts and IPv6 zone identifiers, including proxy CONNECT tunnel
targets. Host normalization now also follows RFC 3986 normalization
rules for percent-encoded octets by decoding percent-encoded unreserved
characters and uppercasing the hexadecimal digits of retained
percent-encoded octets. (#5095)
... (truncated)
Changelog
Sourced from urllib3's
changelog.
2.8.0 (2026-09-15)
Security
Fixed the following security issues:
- The TLS configuration for HTTPS proxies could be ignored or
overridden.
(High severity,
GHSA-8988-9cw3-xx77
<https://github.com/urllib3/urllib3/security/advisories/GHSA-8988-9cw3-xx77>__)
HTTPResponse.stream() and read_chunked()
could buffer a chunk-size
line of unbounded length in memory. (High severity,
GHSA-vxq7-64xx-v4gw
<https://github.com/urllib3/urllib3/security/advisories/GHSA-vxq7-64xx-v4gw>__)
- Chunked Deflate streaming could enter an infinite loop. (Medium
severity,
GHSA-gh4c-6fx4-qh6g
<https://github.com/urllib3/urllib3/security/advisories/GHSA-gh4c-6fx4-qh6g>__)
.. caution::
urllib3 2.8.0 fixes HTTPS proxy TLS configuration being
ignored or
overridden by destination settings. Configurations relying on that
behavior may require changes.
Configure proxy CA certificates and client certificates in
proxy_ssl_context, and proxy identity checks with
proxy_assert_hostname or
proxy_assert_fingerprint.
Destination client certificates and identity overrides no longer
apply to HTTPS forwarding proxy connections.
Deprecations & Removals
- Deprecated using an empty collection as the
Retry
option
allowed_methods to retry any verb.
([#5044](https://github.com/urllib3/urllib3/issues/5044)
<https://github.com/urllib3/urllib3/issues/5044>__)
Features
- Added
Url.auth_decoded and
Url.auth_decoded_joined convenience
properties to the result of parse_url().
([#4945](https://github.com/urllib3/urllib3/issues/4945)
<https://github.com/urllib3/urllib3/issues/4945>__)
- Added
basic_auth_encoding and
proxy_basic_auth_encoding parameters to
urllib3.util.make_headers().
([#5092](https://github.com/urllib3/urllib3/issues/5092)
<https://github.com/urllib3/urllib3/issues/5092>__)
Bugfixes
... (truncated)
Commits
b1d30ab
Release 2.8.0
9016d7e
Skip test_read_chunked_with_trailing_data_does_not_hang for
brotlicffi (#5258)
9101f58
Fix nox -s docs warning (#5256)
cd770b0
Merge commit from fork
ea2ad7b
Merge commit from fork
0716e31
Fix loading unencrypted client keys with a password in pyOpenSSL (#5255)
43c68c8
Test pickling of InvalidChunkLength (#5247)
308b279
Share security policy between GitHub and Read the Docs (#5253)
53fa073
Add policy on duplicate pull requests (#5252)
5f2a6a8
Assert on the ALPN extension in test_tunnel_sets_http_11_alpn (#5232)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/bytefolk/mem/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 修雨 <47820304+PeterGuy326@users.noreply.github.com>
---
worker/uv.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/worker/uv.lock b/worker/uv.lock
index 0bf50a4..82e7d3f 100644
--- a/worker/uv.lock
+++ b/worker/uv.lock
@@ -2520,11 +2520,11 @@ wheels = [
[[package]]
name = "urllib3"
-version = "2.7.0"
+version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
]
[[package]]
From e4e085fff920e3da180dfc5b603508de2b757696 Mon Sep 17 00:00:00 2001
From: sun-970
Date: Thu, 15 Oct 2026 14:43:40 +0800
Subject: [PATCH 5/6] fix(db): resequence hygiene migration to 0028 after main
took 0027
main commit 8c4080a (#235) added 0027_vault_sync.sql, colliding with this
branch's 0027_data_plane_hygiene.sql. Goose v3.22.1 CollectMigrations rejects
the combined set with "goose: duplicate version 27 detected", which blocks
database migration; the previous 21 green checks did not cover this new
combination.
Integrate current main and move the unmerged hygiene migration to the next
available version, 0028:
- rename migrations/0027_data_plane_hygiene.sql -> 0028_data_plane_hygiene.sql
- update migrations_test.go fixture path
- update TestMigrationUpgradeSequence version gates (>= 28) so the upgrade
sequence still asserts 26 producer-agent indexing, 27 vault_sync, then 28
dedupe/unique rejection
- update docs/MIGRATION_SEQUENCE.md: predecessor schema 27, head 24 through 28,
and record why the version moved twice (0026 -> 0027 -> 0028)
Already released migration identities are preserved; 0027 now belongs to
main's vault_sync.
Verification on this head (Go 1.27.0, Windows):
- goose.CollectMigrations over the assembled tree: 28 migrations, no duplicate
version, head 0028 (was: duplicate version 27 panic)
- go build ./... clean
- go test ./internal/db/... ./internal/memory/... ./internal/indexgeneration/...
./internal/api/... pass (excluding the pre-existing Windows-only
TestWorkspaceImportSpoolsSecureTemporaryFile 0600-mode failure, which also
fails on clean main)
---
docs/MIGRATION_SEQUENCE.md | 23 ++++++++++---------
server/internal/db/migration_sequence_test.go | 4 ++--
...ygiene.sql => 0028_data_plane_hygiene.sql} | 0
server/internal/db/migrations_test.go | 2 +-
4 files changed, 15 insertions(+), 14 deletions(-)
rename server/internal/db/migrations/{0027_data_plane_hygiene.sql => 0028_data_plane_hygiene.sql} (100%)
diff --git a/docs/MIGRATION_SEQUENCE.md b/docs/MIGRATION_SEQUENCE.md
index e02332c..8d14a28 100644
--- a/docs/MIGRATION_SEQUENCE.md
+++ b/docs/MIGRATION_SEQUENCE.md
@@ -6,18 +6,19 @@ These draft changes are cumulative, not independently deployable:
| --- | --- | --- | --- |
| 1 | #194 / #183 | 0024 file lexical lane | released/main schema 23 (merged) |
| 2 | #173 HNSW completion (supersedes #197 HOLD) | 0025 HNSW indexes + text continuation | #194, schema 24 |
-| 3 | #195 / #185 | 0027 data-plane hygiene | schema 26 |
+| 3 | #195 / #185 | 0028 data-plane hygiene | schema 27 |
-Migrations 0024–0026 are now on `main`. PR #195 must integrate that exact
-history before adding data-plane hygiene as 0027. Local repair branches retain
+Migrations 0024–0027 are now on `main`. PR #195 must integrate that exact
+history before adding data-plane hygiene as 0028. Local repair branches retain
the original authored commits and add a current-main integration commit rather
than rewriting published history.
-The hygiene migration was originally reviewed as draft 0026, but main now owns
-0026 for `memory_producer_agent`. The unmerged hygiene migration therefore
-moves to 0027. An operator who privately applied the old draft under version 26
-must stop and obtain a recovery plan; do not rewrite Goose history to make the
-new main sequence appear valid.
+The hygiene migration was originally reviewed as draft 0026, then sequenced as
+0027 while main owned only 0026. Main has since taken 0027 for `vault_sync`
+(#235), so the unmerged hygiene migration moves on to 0028 — the next version
+free on main. An operator who privately applied the old draft under version 26
+or 27 must stop and obtain a recovery plan; do not rewrite Goose history to make
+the new main sequence appear valid.
Migration 0025 creates the three cosine HNSW indexes. The shipping text route
no longer uses `DISTINCT ON (f.id) ORDER BY f.id` as its primary plan: it walks
@@ -25,12 +26,12 @@ cosine-ordered candidates and falls back to that exact query only when a bounded
scan underfills. Visual cosine-order already matched HNSW. Face DDL is not a
face-query speedup. #195 still requires predecessor schema 25. Recall and live
latency remain `#175`, not this migration. #195 now requires predecessor schema
-26.
+27.
This document does not waive a review gate or authorize deployment.
Goose startup remains strict: no `WithAllowMissing` or equivalent option is
-enabled. A database that already applied 27 while omitting 24/25/26 will correctly
+enabled. A database that already applied 28 while omitting 24/25/26/27 will correctly
fail startup against the cumulative schema. Stop and obtain an operator-owned
recovery plan for such a database; do not edit its migration history, renumber
its SQL, or apply lower versions out of order to manufacture a pass.
@@ -56,7 +57,7 @@ an operator-owned recovery; do not mark an unverified partial schema applied.
`scripts/verify.sh integration` creates a separate, owned `_test` database and
runs `TestMigrationUpgradeSequence`. It applies real Goose migrations to 23,
seeds a file with duplicate text chunks, then advances one version at a time
-to the branch's declared head (24 through 27). Each step checks full applied
+to the branch's declared head (24 through 28). Each step checks full applied
history and preserved data; subsequent steps check lexical backfill, valid
HNSW DDL, producer-agent indexing, and hygiene deduplication/unique rejection.
Finally the ordinary production
diff --git a/server/internal/db/migration_sequence_test.go b/server/internal/db/migration_sequence_test.go
index 905128d..db9ae76 100644
--- a/server/internal/db/migration_sequence_test.go
+++ b/server/internal/db/migration_sequence_test.go
@@ -110,7 +110,7 @@ func TestMigrationUpgradeSequence(t *testing.T) {
}
var chunks int
wantChunks := 2
- if version >= 27 {
+ if version >= 28 {
wantChunks = 1
}
if err := sqldb.QueryRowContext(ctx, "SELECT count(*) FROM embeddings_text WHERE file_id=$1", fileID).Scan(&chunks); err != nil || chunks != wantChunks {
@@ -131,7 +131,7 @@ func TestMigrationUpgradeSequence(t *testing.T) {
}
t.Logf("PASS: strict Goose upgrade to %d; complete history and populated data preserved", version)
}
- if head >= 27 {
+ if head >= 28 {
_, err := sqldb.ExecContext(ctx, "INSERT INTO embeddings_text(file_id,chunk_index,chunk_text) VALUES($1,0,'duplicate')", fileID)
var pgErr *pgconn.PgError
if !errors.As(err, &pgErr) || pgErr.Code != "23505" {
diff --git a/server/internal/db/migrations/0027_data_plane_hygiene.sql b/server/internal/db/migrations/0028_data_plane_hygiene.sql
similarity index 100%
rename from server/internal/db/migrations/0027_data_plane_hygiene.sql
rename to server/internal/db/migrations/0028_data_plane_hygiene.sql
diff --git a/server/internal/db/migrations_test.go b/server/internal/db/migrations_test.go
index a4c1338..6b1f3dc 100644
--- a/server/internal/db/migrations_test.go
+++ b/server/internal/db/migrations_test.go
@@ -82,7 +82,7 @@ func TestEmbeddingsTextUniqueConstraint(t *testing.T) {
VALUES ($1, 0, 'legacy duplicate')`, fileID); err != nil {
t.Fatal(err)
}
- migration, err := migrationsFS.ReadFile("migrations/0027_data_plane_hygiene.sql")
+ migration, err := migrationsFS.ReadFile("migrations/0028_data_plane_hygiene.sql")
if err != nil {
t.Fatal(err)
}
From 8263a012e905e1bcff32608538438cd18b5857d3 Mon Sep 17 00:00:00 2001
From: sun-970 <3843544764@qq.com>
Date: Thu, 15 Oct 2026 15:41:33 +0800
Subject: [PATCH 6/6] fix(ci): point migration runner at head 28
PostgreSQL integration on the previous head upgraded the populated sequence
through 28 successfully, then failed the runner assertion with
"ERROR: expected migration version 27, got 28" (run 38694417311, PostgreSQL
integration job 116287630741), which also kept the rollback/forward and race
stages from running.
The runner reads one constant, so this is the whole fix:
- scripts/verify.sh: EXPECTED_MIGRATION_HEAD 27 -> 28. All nine
assert_migration_version calls and the up-to target already reference the
variable, so no other line changes. Mode stays 100755.
- docs/TESTING.md: the same stale "declared current head (currently 27)" is
updated to 28 so the documented expectation matches the runner.
Checked there is no remaining hardcoded 27 migration assertion:
grep -rn 'EXPECTED_MIGRATION_HEAD=27' and the TESTING.md phrase both come back
empty. bash -n scripts/verify.sh is clean.
Not claimed: no local PostgreSQL run. The deterministic mismatch this repairs
is the assertion, not the migration path; the PostgreSQL integration and race
stages still need a rerun on the updated head.
---
docs/TESTING.md | 2 +-
scripts/verify.sh | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 480a42e..46f2db8 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -277,7 +277,7 @@ Required tests:
- `TestVaultSyncPostgres` (two-client CAS, distinct empty notes, auth/workspace,
immutable history, tombstones, path swap/title and migration 27 down/up)
-Expected result: migrations reach the declared current head (currently `27`),
+Expected result: migrations reach the declared current head (currently `28`),
all rollback-state assertions pass, every named test prints `PASS`, all commands
exit `0`, and the race run reports no data race.
diff --git a/scripts/verify.sh b/scripts/verify.sh
index d7b3ac6..8ef27cb 100755
--- a/scripts/verify.sh
+++ b/scripts/verify.sh
@@ -4,7 +4,7 @@ set -euo pipefail
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
MODE="${1:-unit}"
-EXPECTED_MIGRATION_HEAD=27
+EXPECTED_MIGRATION_HEAD=28
MIGRATION_ROLLBACK_TARGET=11
MODEL_TEXT_CANONICAL_BASE=15
WORKSPACE_AI_PROFILE_BASE=16