From cf472cf77fb93325858f485a22e967449d7c387a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:54:13 +0900 Subject: [PATCH 01/31] docs(agents): add CWL-ENTRY read-first block .github's AGENTS.md carries a block pointing agents at the master context, live GitHub Project, and gap snapshot before any work. naruon never had this block (it only has a naruon-specific cwl-agent-guidance section below it), so agents landing here had no equivalent pointer. Add naruon's own version, wired to naruon's real docs: docs/architecture/naruon-product-spec.md for mission/architecture, the existing docs/product-technical-gap-baseline.md gap snapshot, and a cross-repo link to .github's agent-github-project-protocol.md and product-goal-directive.md, which are genuinely org-wide and not duplicated per repo. The Figma file ID line reflects naruon's actual current state (not yet canonical on develop; ADR-0013 is still in open PR #1436) rather than the N/A placeholder that would make no sense for a repo that has UI. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 9104dd1f4..ec36650bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,8 @@ # AGENTS.md + +> **Agents: read the master context FIRST.** Before any work, read [`docs/architecture/naruon-product-spec.md`](docs/architecture/naruon-product-spec.md) (product vision & architecture, RBAC/ABAC, core features & AI agent ontology, branding/UX, observability & governance), the live **GitHub Project #1** (work/roadmap source of truth), the live gap snapshot [`docs/product-technical-gap-baseline.md`](docs/product-technical-gap-baseline.md) (not merge authorization; canonical completion issue [#1428](https://github.com/ContextualWisdomLab/naruon/issues/1428); this repo's Figma file ID is not yet canonical on `develop` — the design-system decision (ADR-0013, file ID `68b5XB58w8nwT2LYOOnikK`) is still in open PR [#1436](https://github.com/ContextualWisdomLab/naruon/pull/1436)), and operate the Project per `ContextualWisdomLab/.github`'s [`docs/agent-github-project-protocol.md`](https://github.com/ContextualWisdomLab/.github/blob/main/docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth. The standing autonomous operating directive for the continuous PR review→fix→merge→develop loop across the ecosystem is `ContextualWisdomLab/.github`'s [`docs/product-goal-directive.md`](https://github.com/ContextualWisdomLab/.github/blob/main/docs/product-goal-directive.md) — a `/goal` session's length-capped pointer refers to it; read the full directive before running or configuring any such loop. + ## Agent guidance (CWL governance) From fb2c244c0b9d5459a47ee57c6075907f6ae288c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 05:46:50 +0900 Subject: [PATCH 02/31] test(governance): pin canonical LLM owner guidance RED --- .../tests/test_agent_llm_authority_docs.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 backend/tests/test_agent_llm_authority_docs.py diff --git a/backend/tests/test_agent_llm_authority_docs.py b/backend/tests/test_agent_llm_authority_docs.py new file mode 100644 index 000000000..8360e55a0 --- /dev/null +++ b/backend/tests/test_agent_llm_authority_docs.py @@ -0,0 +1,41 @@ +"""Regression contract for current LLM-routing authority guidance.""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _read(path: str) -> str: + """Read repository guidance as UTF-8 text.""" + return (REPO_ROOT / path).read_text(encoding="utf-8") + + +def test_agent_guidance_does_not_reintroduce_direct_model_routing_authority() -> None: + """Current agent guidance must not prescribe direct provider/model routing.""" + agents = _read("AGENTS.md") + claude = _read("CLAUDE.md") + + forbidden_current_guidance = ( + "STRIX_GITHUB_MODELS_TOKEN", + "https://models.github.ai/inference", + "Direct OpenAI GPT-5.4-or-newer", + "OpenAI-compatible LLM providers", + ) + combined = f"{agents}\n{claude}" + for phrase in forbidden_current_guidance: + assert phrase not in combined + + +def test_agent_guidance_names_canonical_llm_owner_and_fail_closed_boundary() -> None: + """Guidance must preserve product ownership while delegating LLM routing.""" + agents = _read("AGENTS.md") + claude = _read("CLAUDE.md") + + assert "ContextualWisdomLab/.github" in agents + assert "contextual-orchestrator" in agents + assert "orchestrator/free" in agents + assert "fail closed" in agents.lower() + assert "contextual-orchestrator" in claude + assert "Naruon owns" in claude + assert "provider/model routing" in claude From 45f115de3c1f019daf51551fd4e475b6c7fb1937 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 05:50:38 +0900 Subject: [PATCH 03/31] chore(governance): run bounded guidance source repair --- .../temporary-agent-guidance-repair.yml | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .github/workflows/temporary-agent-guidance-repair.yml diff --git a/.github/workflows/temporary-agent-guidance-repair.yml b/.github/workflows/temporary-agent-guidance-repair.yml new file mode 100644 index 000000000..d065d1dc1 --- /dev/null +++ b/.github/workflows/temporary-agent-guidance-repair.yml @@ -0,0 +1,105 @@ +name: Temporary agent guidance repair + +on: + push: + branches: + - docs/align-llm-owner-guidance + +permissions: + contents: write + +jobs: + repair: + if: ${{ !contains(github.event.head_commit.message, '[agent-guidance-repaired]') }} + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout repair branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: docs/align-llm-owner-guidance + fetch-depth: 1 + + - name: Apply bounded guidance repair + shell: bash + run: | + python - <<'PY' + from pathlib import Path + import re + + agents_path = Path('AGENTS.md') + agents = agents_path.read_text(encoding='utf-8') + + central_pattern = re.compile( + r"- The central Strix Security Scan uses GitHub Models by default through\n" + r".*?" + r" examples cannot re-enter copied workflow guidance\.\n", + re.DOTALL, + ) + central_replacement = """- Central model-backed review workflows are owned by `ContextualWisdomLab/.github`\n and consume the canonical `contextual-orchestrator` sidecar through the fixed\n `orchestrator/free` capability alias. Naruon does not choose a provider, model,\n group, or paid fallback for those lanes and must not copy central routing logic\n into repository-local workflows. Provider discovery, capability routing,\n fallback, pricing/latency/availability policy, and provider credentials stay in\n `contextual-orchestrator`. If that capability is unavailable or its verdict\n cannot be validated, the required lane fails closed and the repair belongs to\n the canonical owner path; do not add a direct-provider escape hatch.\n""" + agents, count = central_pattern.subn(central_replacement, agents, count=1) + if count != 1: + raise SystemExit(f'expected one stale central Strix block, replaced {count}') + + agents = agents.replace( + "- When robot review cites an obsolete Strix provider policy, update the docs and\n" + " tests to the current GitHub Models default contract before accepting a\n" + " rollback suggestion; do not reintroduce generic `LLM_API_KEY` or\n" + " cross-provider credential forwarding while trying to satisfy old comments.\n", + "- When robot review cites an obsolete Strix provider policy, update the docs and\n" + " tests to the current canonical `contextual-orchestrator` / `orchestrator/free`\n" + " contract before accepting a rollback suggestion; do not reintroduce generic\n" + " provider keys, direct-provider routing, or cross-provider credential forwarding\n" + " while trying to satisfy old comments.\n", + ) + agents = agents.replace( + " external scanner such as direct-OpenAI Strix is quota-blocked after comments\n", + " external required scanner is infrastructure-blocked after comments\n", + ) + + authority_marker = "### Research grounding (attach paper PDFs)\n" + authority_block = """### LLM authority boundary\n\n- Naruon owns product/domain truth, tools, authorization, and context assembly;\n it does not own provider/model routing. Production LLM capability must consume\n an immutable released `contextual-orchestrator` API/client/schema through an\n explicit ACL. When that immutable consumer contract is unavailable, fail closed\n rather than bind to a mutable owner head or a direct provider.\n- The central `.github` review control plane is a separate owner path and uses\n `orchestrator/free`; that GitHub Actions policy is not a Naruon runtime pool\n selector. Explicit local deterministic fixtures and local model development\n stacks may remain non-production evidence only and must never become a silent\n production fallback.\n\n""" + if authority_block not in agents: + if authority_marker not in agents: + raise SystemExit('LLM authority insertion marker missing') + agents = agents.replace(authority_marker, authority_block + authority_marker, 1) + + agents_path.write_text(agents, encoding='utf-8') + + claude_path = Path('CLAUDE.md') + claude = claude_path.read_text(encoding='utf-8') + old_diagram = """Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgvector\n │\n ├──> OpenAI-compatible LLM providers (Ollama locally)\n └──> outbound-only self-hosted connector (connector/)\n └──> customer IMAP/POP3/SMTP + CalDAV/CardDAV/WebDAV\n""" + new_diagram = """Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgvector\n │\n ├──> contextual-orchestrator released consumer contract\n │ └──> provider/model routing owned by contextual-orchestrator\n └──> outbound-only self-hosted connector (connector/)\n └──> customer IMAP/POP3/SMTP + CalDAV/CardDAV/WebDAV\n""" + if old_diagram not in claude: + raise SystemExit('stale CLAUDE architecture diagram missing') + claude = claude.replace(old_diagram, new_diagram, 1) + + auth_old = """- Auth: signed HS256 bearer sessions (HMAC via `AUTH_SESSION_HMAC_SECRET`) or\n enterprise OIDC/JWKS; private `/api/*` routers register the default\n `get_auth_context` dependency. LLM `base_url` and OIDC/SMTP/IMAP/POP3 hosts\n are strict egress allowlists that resolve only to pinned global addresses.\n""" + auth_new = """- Auth: signed HS256 bearer sessions (HMAC via `AUTH_SESSION_HMAC_SECRET`) or\n enterprise OIDC/JWKS; private `/api/*` routers register the default\n `get_auth_context` dependency. Naruon owns domain tools, authorization, and\n context assembly; provider/model routing belongs to `contextual-orchestrator`.\n Existing direct-provider `base_url` surfaces are legacy migration debt and must\n not be extended as production routing authority; keep their SSRF controls until\n they are removed. OIDC/SMTP/IMAP/POP3 hosts remain strict egress allowlists\n that resolve only to pinned global addresses.\n""" + if auth_old not in claude: + raise SystemExit('stale CLAUDE auth/base_url block missing') + claude = claude.replace(auth_old, auth_new, 1) + + architecture_note = """\nNaruon owns product behavior and model-call authorization, but not provider/model routing.\nProduction model work requires an immutable released `contextual-orchestrator`\nAPI/client/schema; a missing or incompatible owner contract fails closed instead\nof falling back to a direct provider. The Ollama/MLX paths in local Compose are\nexplicit non-production development fixtures. Central model-backed GitHub Actions\nare owned by `ContextualWisdomLab/.github` and use `orchestrator/free`; do not copy\nthat pool selection into Naruon runtime.\n""" + fence = new_diagram + "```\n" + if architecture_note not in claude: + if fence not in claude: + raise SystemExit('updated CLAUDE diagram fence missing') + claude = claude.replace(fence, fence + architecture_note, 1) + + claude_path.write_text(claude, encoding='utf-8') + PY + + rm .github/workflows/temporary-agent-guidance-repair.yml + python -m pytest backend/tests/test_agent_llm_authority_docs.py -q + git diff --check + + git config user.name "Naruon commercial writer" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add AGENTS.md CLAUDE.md backend/tests/test_agent_llm_authority_docs.py .github/workflows/temporary-agent-guidance-repair.yml + git commit -m "docs(governance): align LLM authority guidance [agent-guidance-repaired]" + git push origin HEAD:docs/align-llm-owner-guidance From dae59c451c87eb9044b1c804c75c05a2f1f6a5d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 06:43:08 +0900 Subject: [PATCH 04/31] ci(governance): remove stalled temporary guidance repair workflow --- .../temporary-agent-guidance-repair.yml | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 .github/workflows/temporary-agent-guidance-repair.yml diff --git a/.github/workflows/temporary-agent-guidance-repair.yml b/.github/workflows/temporary-agent-guidance-repair.yml deleted file mode 100644 index d065d1dc1..000000000 --- a/.github/workflows/temporary-agent-guidance-repair.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Temporary agent guidance repair - -on: - push: - branches: - - docs/align-llm-owner-guidance - -permissions: - contents: write - -jobs: - repair: - if: ${{ !contains(github.event.head_commit.message, '[agent-guidance-repaired]') }} - runs-on: ubuntu-latest - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - - name: Checkout repair branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: docs/align-llm-owner-guidance - fetch-depth: 1 - - - name: Apply bounded guidance repair - shell: bash - run: | - python - <<'PY' - from pathlib import Path - import re - - agents_path = Path('AGENTS.md') - agents = agents_path.read_text(encoding='utf-8') - - central_pattern = re.compile( - r"- The central Strix Security Scan uses GitHub Models by default through\n" - r".*?" - r" examples cannot re-enter copied workflow guidance\.\n", - re.DOTALL, - ) - central_replacement = """- Central model-backed review workflows are owned by `ContextualWisdomLab/.github`\n and consume the canonical `contextual-orchestrator` sidecar through the fixed\n `orchestrator/free` capability alias. Naruon does not choose a provider, model,\n group, or paid fallback for those lanes and must not copy central routing logic\n into repository-local workflows. Provider discovery, capability routing,\n fallback, pricing/latency/availability policy, and provider credentials stay in\n `contextual-orchestrator`. If that capability is unavailable or its verdict\n cannot be validated, the required lane fails closed and the repair belongs to\n the canonical owner path; do not add a direct-provider escape hatch.\n""" - agents, count = central_pattern.subn(central_replacement, agents, count=1) - if count != 1: - raise SystemExit(f'expected one stale central Strix block, replaced {count}') - - agents = agents.replace( - "- When robot review cites an obsolete Strix provider policy, update the docs and\n" - " tests to the current GitHub Models default contract before accepting a\n" - " rollback suggestion; do not reintroduce generic `LLM_API_KEY` or\n" - " cross-provider credential forwarding while trying to satisfy old comments.\n", - "- When robot review cites an obsolete Strix provider policy, update the docs and\n" - " tests to the current canonical `contextual-orchestrator` / `orchestrator/free`\n" - " contract before accepting a rollback suggestion; do not reintroduce generic\n" - " provider keys, direct-provider routing, or cross-provider credential forwarding\n" - " while trying to satisfy old comments.\n", - ) - agents = agents.replace( - " external scanner such as direct-OpenAI Strix is quota-blocked after comments\n", - " external required scanner is infrastructure-blocked after comments\n", - ) - - authority_marker = "### Research grounding (attach paper PDFs)\n" - authority_block = """### LLM authority boundary\n\n- Naruon owns product/domain truth, tools, authorization, and context assembly;\n it does not own provider/model routing. Production LLM capability must consume\n an immutable released `contextual-orchestrator` API/client/schema through an\n explicit ACL. When that immutable consumer contract is unavailable, fail closed\n rather than bind to a mutable owner head or a direct provider.\n- The central `.github` review control plane is a separate owner path and uses\n `orchestrator/free`; that GitHub Actions policy is not a Naruon runtime pool\n selector. Explicit local deterministic fixtures and local model development\n stacks may remain non-production evidence only and must never become a silent\n production fallback.\n\n""" - if authority_block not in agents: - if authority_marker not in agents: - raise SystemExit('LLM authority insertion marker missing') - agents = agents.replace(authority_marker, authority_block + authority_marker, 1) - - agents_path.write_text(agents, encoding='utf-8') - - claude_path = Path('CLAUDE.md') - claude = claude_path.read_text(encoding='utf-8') - old_diagram = """Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgvector\n │\n ├──> OpenAI-compatible LLM providers (Ollama locally)\n └──> outbound-only self-hosted connector (connector/)\n └──> customer IMAP/POP3/SMTP + CalDAV/CardDAV/WebDAV\n""" - new_diagram = """Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgvector\n │\n ├──> contextual-orchestrator released consumer contract\n │ └──> provider/model routing owned by contextual-orchestrator\n └──> outbound-only self-hosted connector (connector/)\n └──> customer IMAP/POP3/SMTP + CalDAV/CardDAV/WebDAV\n""" - if old_diagram not in claude: - raise SystemExit('stale CLAUDE architecture diagram missing') - claude = claude.replace(old_diagram, new_diagram, 1) - - auth_old = """- Auth: signed HS256 bearer sessions (HMAC via `AUTH_SESSION_HMAC_SECRET`) or\n enterprise OIDC/JWKS; private `/api/*` routers register the default\n `get_auth_context` dependency. LLM `base_url` and OIDC/SMTP/IMAP/POP3 hosts\n are strict egress allowlists that resolve only to pinned global addresses.\n""" - auth_new = """- Auth: signed HS256 bearer sessions (HMAC via `AUTH_SESSION_HMAC_SECRET`) or\n enterprise OIDC/JWKS; private `/api/*` routers register the default\n `get_auth_context` dependency. Naruon owns domain tools, authorization, and\n context assembly; provider/model routing belongs to `contextual-orchestrator`.\n Existing direct-provider `base_url` surfaces are legacy migration debt and must\n not be extended as production routing authority; keep their SSRF controls until\n they are removed. OIDC/SMTP/IMAP/POP3 hosts remain strict egress allowlists\n that resolve only to pinned global addresses.\n""" - if auth_old not in claude: - raise SystemExit('stale CLAUDE auth/base_url block missing') - claude = claude.replace(auth_old, auth_new, 1) - - architecture_note = """\nNaruon owns product behavior and model-call authorization, but not provider/model routing.\nProduction model work requires an immutable released `contextual-orchestrator`\nAPI/client/schema; a missing or incompatible owner contract fails closed instead\nof falling back to a direct provider. The Ollama/MLX paths in local Compose are\nexplicit non-production development fixtures. Central model-backed GitHub Actions\nare owned by `ContextualWisdomLab/.github` and use `orchestrator/free`; do not copy\nthat pool selection into Naruon runtime.\n""" - fence = new_diagram + "```\n" - if architecture_note not in claude: - if fence not in claude: - raise SystemExit('updated CLAUDE diagram fence missing') - claude = claude.replace(fence, fence + architecture_note, 1) - - claude_path.write_text(claude, encoding='utf-8') - PY - - rm .github/workflows/temporary-agent-guidance-repair.yml - python -m pytest backend/tests/test_agent_llm_authority_docs.py -q - git diff --check - - git config user.name "Naruon commercial writer" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add AGENTS.md CLAUDE.md backend/tests/test_agent_llm_authority_docs.py .github/workflows/temporary-agent-guidance-repair.yml - git commit -m "docs(governance): align LLM authority guidance [agent-guidance-repaired]" - git push origin HEAD:docs/align-llm-owner-guidance From 853f7a6540ad16ea0099d3eba153558959ef3a4d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 07:43:48 +0900 Subject: [PATCH 05/31] docs(governance): align Claude LLM authority boundary --- CLAUDE.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index be67bc80c..8ff1f5815 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ uvicorn main:app --reload # local dev server only - `scripts/bootstrap_db.py` is the local/dev-only schema compatibility path; Alembic history under `backend/alembic` is authoritative. -### Frontend (Next.js, in `frontend/`, pnpm@11.5.3) +### Frontend (Next.js, Python, in `frontend/`, pnpm@11.5.3) ```bash cd frontend @@ -110,11 +110,20 @@ indexes, and auditable writeback intent. `ARCHITECTURE.md` and ``` Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgvector │ - ├──> OpenAI-compatible LLM providers (Ollama locally) + ├──> contextual-orchestrator released consumer contract + │ └──> provider/model routing owned by contextual-orchestrator └──> outbound-only self-hosted connector (connector/) └──> customer IMAP/POP3/SMTP + CalDAV/CardDAV/WebDAV ``` +Naruon owns product behavior and model-call authorization, but not provider/model routing. +Production model work requires an immutable released `contextual-orchestrator` +API/client/schema; a missing or incompatible owner contract fails closed instead +of falling back to a direct provider. The Ollama/MLX paths in local Compose are +explicit non-production development fixtures. Central model-backed GitHub Actions +are owned by `ContextualWisdomLab/.github` and use `orchestrator/free`; do not copy +that pool selection into Naruon runtime. + - `backend/` — FastAPI app (`main.py`, routers in `api/`, domain logic in `services/`, SQLAlchemy models in `db/`, Alembic in `alembic/`). Owns persistence, canonical email threading @@ -140,8 +149,12 @@ Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgve ETag/If-Match); intents are the default response. - Auth: signed HS256 bearer sessions (HMAC via `AUTH_SESSION_HMAC_SECRET`) or enterprise OIDC/JWKS; private `/api/*` routers register the default - `get_auth_context` dependency. LLM `base_url` and OIDC/SMTP/IMAP/POP3 hosts - are strict egress allowlists that resolve only to pinned global addresses. + `get_auth_context` dependency. Naruon owns domain tools, authorization, and + context assembly; provider/model routing belongs to `contextual-orchestrator`. + Existing direct-provider `base_url` surfaces are legacy migration debt and must + not be extended as production routing authority; keep their SSRF controls until + they are removed. OIDC/SMTP/IMAP/POP3 hosts remain strict egress allowlists + that resolve only to pinned global addresses. - CI (`.github/workflows/`): `app-ci.yml` (backend ruff+pytest, frontend test/lint/build), plus `bandit`, `codeql`, `trivy`, `scorecard`, `pr-governance`, `docker-publish` (GHCR on `v*` tags matching `VERSION`), and @@ -175,4 +188,4 @@ Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgve FastAPI app metadata synchronized; the backend reads its version from `VERSION`. - When a review uncovers a recurring bug pattern, record the anti-pattern in - `AGENTS.md` and update the affected tests/mocks/docs in the same PR. + `AGENTS.md` and update the affected tests/mocks/docs in the same PR. \ No newline at end of file From 75e6c09c03dcb79cedbff60dfd87696a9ec60f71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 07:44:08 +0900 Subject: [PATCH 06/31] docs(governance): preserve frontend guidance wording --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8ff1f5815..921f61cd1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ uvicorn main:app --reload # local dev server only - `scripts/bootstrap_db.py` is the local/dev-only schema compatibility path; Alembic history under `backend/alembic` is authoritative. -### Frontend (Next.js, Python, in `frontend/`, pnpm@11.5.3) +### Frontend (Next.js, in `frontend/`, pnpm@11.5.3) ```bash cd frontend From 6ca9879c1bb55b7240f9adfa8585510568ec887d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 07:48:26 +0900 Subject: [PATCH 07/31] docs(governance): align agent LLM authority boundary --- AGENTS.md | 84 ++++++++++++++++++------------------------------------- 1 file changed, 27 insertions(+), 57 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9104dd1f4..48e318782 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,19 @@ in this repo. PDF→DOM sidecar; **semantic-data-portal** = upper ontology/catalog/governance plane with its own graph engine. +### LLM authority boundary + +- Naruon owns product/domain truth, tools, authorization, and context assembly; + it does not own provider/model routing. Production LLM capability must consume + an immutable released `contextual-orchestrator` API/client/schema through an + explicit ACL. When that immutable consumer contract is unavailable, fail closed + rather than bind to a mutable owner head or a direct provider. +- The central `.github` review control plane is a separate owner path and uses + `orchestrator/free`; that GitHub Actions policy is not a Naruon runtime pool + selector. Explicit local deterministic fixtures and local model development + stacks may remain non-production evidence only and must never become a silent + production fallback. + ### Research grounding (attach paper PDFs) - **Org rule:** substantive feature or process PRs should find the relevant @@ -136,51 +149,15 @@ in this repo. `.github/workflows/opencode-review.yml`, `.github/workflows/strix.yml`, `.github/workflows/strix-selftest.yml`, or `.github/workflows/pr-review-merge-scheduler.yml`. -- The central Strix Security Scan uses GitHub Models by default through - `STRIX_GITHUB_MODELS_TOKEN`, `STRIX_LLM=openai/gpt-5`, and - `LLM_API_BASE_FILE` pointing at a trusted file containing - `https://models.github.ai/inference`; GitHub Models scans must try the - configured GPT-5-or-newer model first and may fall back to the explicit - workflow fallback list, currently - `github_models/deepseek/deepseek-r1-0528` and - `github_models/deepseek/deepseek-v3-0324`, when GitHub Models provider - capacity or model availability blocks the primary run. The Strix gate must - route these fallback names through the GitHub Models endpoint with - OpenAI-compatible child model names such as - `openai/deepseek/deepseek-r1-0528`, not the public DeepSeek API. Do not use - GPT-4.1 or weaker GitHub Models fallbacks for Strix or OpenCode PR review - evidence. Keep the GitHub Models endpoint in a trusted input file and pass - the token only through - the provider-scoped Strix child-process key path. Legacy `STRIX_LLM` secrets - must not override PR, push, or scheduled Strix defaults. Vertex remains - available only for manual - `workflow_dispatch` evidence when the `strix_llm` input - explicitly selects `vertex_ai/gemini-3.1-pro-preview-customtools` or - `vertex_ai/gemini-2.5-flash` with `GCP_SA_KEY`; expose Google/Vertex - credentials only for Vertex provider mode. Direct OpenAI GPT-5.4-or-newer - scans remain supported only for manual `strix_llm` selections with - `STRIX_OPENAI_API_KEY`. Do not silently fall back between providers, and - do not treat timeout-class provider infrastructure failures as clean PR - evidence even when Strix printed zero vulnerabilities before failing. Disable - silent Vertex fallback models in the workflow unless a future PR proves a new - exact fallback contract with no Timeout/Fatal/Warn/Denied output. Record - provider evidence in the PR. Known third-party Strix/Pydantic - serializer warnings must be filtered narrowly inside the Strix gate child - process, not as a visible workflow env entry, so Warn-class logs are not - accepted as clean evidence and warning-filter variable names do not pollute - GitHub logs. Strix workflow runtime budget keys should be exported inside the - execution shell, not listed as visible step `env:` timeout names, so clean runs - do not carry stale timeout-signal strings. Keep PR-scope process budgets large - enough for Strix to finalize reports after the scanner emits completion - events; a wrapper timeout after `vulnerability_count: 0` is still failed - evidence, not a pass. PR evidence must present the full scannable changed-file - set from the PR head, plus allowlisted trusted context files, to Strix in one - scanner invocation; do not split changed files into separate scanner runs or - copy the entire PR-head repository tree by default because either breaks - Strix's required whole-context and bounded-input contract. Keep architecture - docs and reusable Strix gate tests aligned with this rule so stale - Vertex-default, OpenAI-only, unavailable-model, blanket-warning, or generic-key - examples cannot re-enter copied workflow guidance. +- Central model-backed review workflows are owned by `ContextualWisdomLab/.github` + and consume the canonical `contextual-orchestrator` sidecar through the fixed + `orchestrator/free` capability alias. Naruon does not choose a provider, model, + group, or paid fallback for those lanes and must not copy central routing logic + into repository-local workflows. Provider discovery, capability routing, + fallback, pricing/latency/availability policy, and provider credentials stay in + `contextual-orchestrator`. If that capability is unavailable or its verdict + cannot be validated, the required lane fails closed and the repair belongs to + the canonical owner path; do not add a direct-provider escape hatch. - HMAC fallback sessions are local/control-plane compatibility credentials, not authoritative workspace-membership evidence. Sensitive tenant security posture surfaces must require OIDC/JWKS-backed membership or an explicit dependency @@ -258,7 +235,7 @@ in this repo. keep CodeRabbit GitHub Checks integration disabled. GitHub Actions are already evaluated by required checks and PR Governance; letting CodeRabbit also gate on Actions can strand a stale GitHub `CHANGES_REQUESTED` review when an - external scanner such as direct-OpenAI Strix is quota-blocked after comments + external required scanner is infrastructure-blocked after comments are fixed. - `STARTUP_FAILURE` in required PR governance/check metadata is a hard blocker and should use the same idempotent metadata-gate comment path. @@ -458,9 +435,10 @@ in this repo. responses must include `Referrer-Policy`, and `target="_blank"` links must use explicit `rel="noopener noreferrer"`. - When robot review cites an obsolete Strix provider policy, update the docs and - tests to the current GitHub Models default contract before accepting a - rollback suggestion; do not reintroduce generic `LLM_API_KEY` or - cross-provider credential forwarding while trying to satisfy old comments. + tests to the current canonical `contextual-orchestrator` / `orchestrator/free` + contract before accepting a rollback suggestion; do not reintroduce generic + provider keys, direct-provider routing, or cross-provider credential forwarding + while trying to satisfy old comments. - When reviews find inert navigation/dead-space controls, either wire them to an implemented workspace route/API or remove the control; do not leave high-traffic drawer/sidebar entries as permanent `준비 중` copy. @@ -717,11 +695,3 @@ in this repo. - **Goal**: Actively manage tasks to ensure open PR counts converge to 0. - When the gate exhausts fallbacks after the primary model produces a finding at or above threshold and then fails with a retryable error (like `NOT_FOUND`), ensure the final output explicitly reports `Strix quick scan failed with a non-recoverable error.` to prevent downgrading the finding to pass or misleadingly reporting an unavailability error. - -## Code-owner review gates — disabled (on hold) - -As of 2026-08-04, code-owner review requirements (`require_code_owner_reviews` in branch -protection, `require_code_owner_review` in rulesets) are disabled across the ContextualWisdomLab -org: there is a single maintainer (solo developer), so a code-owner approval gate can never be -satisfied. This is ON HOLD until the org has multiple maintainers — do NOT re-enable these -settings or add CODEOWNERS-based merge gates before then. From a2b0a315d68fd5dbe070af0d07d8c6febef3911f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 07:48:55 +0900 Subject: [PATCH 08/31] docs(governance): restore complete AGENTS guidance after bounded edit --- AGENTS.md | 84 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 48e318782..9104dd1f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,19 +84,6 @@ in this repo. PDF→DOM sidecar; **semantic-data-portal** = upper ontology/catalog/governance plane with its own graph engine. -### LLM authority boundary - -- Naruon owns product/domain truth, tools, authorization, and context assembly; - it does not own provider/model routing. Production LLM capability must consume - an immutable released `contextual-orchestrator` API/client/schema through an - explicit ACL. When that immutable consumer contract is unavailable, fail closed - rather than bind to a mutable owner head or a direct provider. -- The central `.github` review control plane is a separate owner path and uses - `orchestrator/free`; that GitHub Actions policy is not a Naruon runtime pool - selector. Explicit local deterministic fixtures and local model development - stacks may remain non-production evidence only and must never become a silent - production fallback. - ### Research grounding (attach paper PDFs) - **Org rule:** substantive feature or process PRs should find the relevant @@ -149,15 +136,51 @@ in this repo. `.github/workflows/opencode-review.yml`, `.github/workflows/strix.yml`, `.github/workflows/strix-selftest.yml`, or `.github/workflows/pr-review-merge-scheduler.yml`. -- Central model-backed review workflows are owned by `ContextualWisdomLab/.github` - and consume the canonical `contextual-orchestrator` sidecar through the fixed - `orchestrator/free` capability alias. Naruon does not choose a provider, model, - group, or paid fallback for those lanes and must not copy central routing logic - into repository-local workflows. Provider discovery, capability routing, - fallback, pricing/latency/availability policy, and provider credentials stay in - `contextual-orchestrator`. If that capability is unavailable or its verdict - cannot be validated, the required lane fails closed and the repair belongs to - the canonical owner path; do not add a direct-provider escape hatch. +- The central Strix Security Scan uses GitHub Models by default through + `STRIX_GITHUB_MODELS_TOKEN`, `STRIX_LLM=openai/gpt-5`, and + `LLM_API_BASE_FILE` pointing at a trusted file containing + `https://models.github.ai/inference`; GitHub Models scans must try the + configured GPT-5-or-newer model first and may fall back to the explicit + workflow fallback list, currently + `github_models/deepseek/deepseek-r1-0528` and + `github_models/deepseek/deepseek-v3-0324`, when GitHub Models provider + capacity or model availability blocks the primary run. The Strix gate must + route these fallback names through the GitHub Models endpoint with + OpenAI-compatible child model names such as + `openai/deepseek/deepseek-r1-0528`, not the public DeepSeek API. Do not use + GPT-4.1 or weaker GitHub Models fallbacks for Strix or OpenCode PR review + evidence. Keep the GitHub Models endpoint in a trusted input file and pass + the token only through + the provider-scoped Strix child-process key path. Legacy `STRIX_LLM` secrets + must not override PR, push, or scheduled Strix defaults. Vertex remains + available only for manual + `workflow_dispatch` evidence when the `strix_llm` input + explicitly selects `vertex_ai/gemini-3.1-pro-preview-customtools` or + `vertex_ai/gemini-2.5-flash` with `GCP_SA_KEY`; expose Google/Vertex + credentials only for Vertex provider mode. Direct OpenAI GPT-5.4-or-newer + scans remain supported only for manual `strix_llm` selections with + `STRIX_OPENAI_API_KEY`. Do not silently fall back between providers, and + do not treat timeout-class provider infrastructure failures as clean PR + evidence even when Strix printed zero vulnerabilities before failing. Disable + silent Vertex fallback models in the workflow unless a future PR proves a new + exact fallback contract with no Timeout/Fatal/Warn/Denied output. Record + provider evidence in the PR. Known third-party Strix/Pydantic + serializer warnings must be filtered narrowly inside the Strix gate child + process, not as a visible workflow env entry, so Warn-class logs are not + accepted as clean evidence and warning-filter variable names do not pollute + GitHub logs. Strix workflow runtime budget keys should be exported inside the + execution shell, not listed as visible step `env:` timeout names, so clean runs + do not carry stale timeout-signal strings. Keep PR-scope process budgets large + enough for Strix to finalize reports after the scanner emits completion + events; a wrapper timeout after `vulnerability_count: 0` is still failed + evidence, not a pass. PR evidence must present the full scannable changed-file + set from the PR head, plus allowlisted trusted context files, to Strix in one + scanner invocation; do not split changed files into separate scanner runs or + copy the entire PR-head repository tree by default because either breaks + Strix's required whole-context and bounded-input contract. Keep architecture + docs and reusable Strix gate tests aligned with this rule so stale + Vertex-default, OpenAI-only, unavailable-model, blanket-warning, or generic-key + examples cannot re-enter copied workflow guidance. - HMAC fallback sessions are local/control-plane compatibility credentials, not authoritative workspace-membership evidence. Sensitive tenant security posture surfaces must require OIDC/JWKS-backed membership or an explicit dependency @@ -235,7 +258,7 @@ in this repo. keep CodeRabbit GitHub Checks integration disabled. GitHub Actions are already evaluated by required checks and PR Governance; letting CodeRabbit also gate on Actions can strand a stale GitHub `CHANGES_REQUESTED` review when an - external required scanner is infrastructure-blocked after comments + external scanner such as direct-OpenAI Strix is quota-blocked after comments are fixed. - `STARTUP_FAILURE` in required PR governance/check metadata is a hard blocker and should use the same idempotent metadata-gate comment path. @@ -435,10 +458,9 @@ in this repo. responses must include `Referrer-Policy`, and `target="_blank"` links must use explicit `rel="noopener noreferrer"`. - When robot review cites an obsolete Strix provider policy, update the docs and - tests to the current canonical `contextual-orchestrator` / `orchestrator/free` - contract before accepting a rollback suggestion; do not reintroduce generic - provider keys, direct-provider routing, or cross-provider credential forwarding - while trying to satisfy old comments. + tests to the current GitHub Models default contract before accepting a + rollback suggestion; do not reintroduce generic `LLM_API_KEY` or + cross-provider credential forwarding while trying to satisfy old comments. - When reviews find inert navigation/dead-space controls, either wire them to an implemented workspace route/API or remove the control; do not leave high-traffic drawer/sidebar entries as permanent `준비 중` copy. @@ -695,3 +717,11 @@ in this repo. - **Goal**: Actively manage tasks to ensure open PR counts converge to 0. - When the gate exhausts fallbacks after the primary model produces a finding at or above threshold and then fails with a retryable error (like `NOT_FOUND`), ensure the final output explicitly reports `Strix quick scan failed with a non-recoverable error.` to prevent downgrading the finding to pass or misleadingly reporting an unavailability error. + +## Code-owner review gates — disabled (on hold) + +As of 2026-08-04, code-owner review requirements (`require_code_owner_reviews` in branch +protection, `require_code_owner_review` in rulesets) are disabled across the ContextualWisdomLab +org: there is a single maintainer (solo developer), so a code-owner approval gate can never be +satisfied. This is ON HOLD until the org has multiple maintainers — do NOT re-enable these +settings or add CODEOWNERS-based merge gates before then. From a8c35039b250195ed8c223f9223eb38b183cae13 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:28:35 +0900 Subject: [PATCH 09/31] docs(governance): codify canonical model owner boundary Remove stale provider-specific routing instructions because those decisions belong to the central workflow and contextual-orchestrator owners. Preserve the scanner evidence and warning rules while recording fail-closed release, credential, and timeout boundaries. --- AGENTS.md | 54 ++++++++----------- CLAUDE.md | 8 +-- .../tests/test_agent_llm_authority_docs.py | 5 ++ 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9104dd1f4..b4c634f1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -136,35 +136,23 @@ in this repo. `.github/workflows/opencode-review.yml`, `.github/workflows/strix.yml`, `.github/workflows/strix-selftest.yml`, or `.github/workflows/pr-review-merge-scheduler.yml`. -- The central Strix Security Scan uses GitHub Models by default through - `STRIX_GITHUB_MODELS_TOKEN`, `STRIX_LLM=openai/gpt-5`, and - `LLM_API_BASE_FILE` pointing at a trusted file containing - `https://models.github.ai/inference`; GitHub Models scans must try the - configured GPT-5-or-newer model first and may fall back to the explicit - workflow fallback list, currently - `github_models/deepseek/deepseek-r1-0528` and - `github_models/deepseek/deepseek-v3-0324`, when GitHub Models provider - capacity or model availability blocks the primary run. The Strix gate must - route these fallback names through the GitHub Models endpoint with - OpenAI-compatible child model names such as - `openai/deepseek/deepseek-r1-0528`, not the public DeepSeek API. Do not use - GPT-4.1 or weaker GitHub Models fallbacks for Strix or OpenCode PR review - evidence. Keep the GitHub Models endpoint in a trusted input file and pass - the token only through - the provider-scoped Strix child-process key path. Legacy `STRIX_LLM` secrets - must not override PR, push, or scheduled Strix defaults. Vertex remains - available only for manual - `workflow_dispatch` evidence when the `strix_llm` input - explicitly selects `vertex_ai/gemini-3.1-pro-preview-customtools` or - `vertex_ai/gemini-2.5-flash` with `GCP_SA_KEY`; expose Google/Vertex - credentials only for Vertex provider mode. Direct OpenAI GPT-5.4-or-newer - scans remain supported only for manual `strix_llm` selections with - `STRIX_OPENAI_API_KEY`. Do not silently fall back between providers, and - do not treat timeout-class provider infrastructure failures as clean PR - evidence even when Strix printed zero vulnerabilities before failing. Disable - silent Vertex fallback models in the workflow unless a future PR proves a new - exact fallback contract with no Timeout/Fatal/Warn/Denied output. Record - provider evidence in the PR. Known third-party Strix/Pydantic +- Central model-backed Actions are owned by `ContextualWisdomLab/.github` and + request only the logical `orchestrator/free` pool with a gateway token. Thin + callers in this repository must not select or forward provider names, model + names, provider groups, endpoints, or paid fallback credentials. +- `ContextualWisdomLab/contextual-orchestrator` owns provider discovery, + capability-based routing, free-pool membership, and fallback. Naruon owns + product/domain truth, authorization, tools, and context assembly. Production + use requires an immutable released owner API/client/schema; verify protected + release evidence before adoption and fail closed when that contract or a + required capability is unavailable. Do not copy owner source, query owner + storage, or treat an open PR or unreleased branch as a consumable contract. +- Do not impose a shared application/agent/gateway wall-clock timeout on model + work. A configured administrator limit, explicit user cancellation, or the + upstream provider's terminal result may end it; reasoning, streaming, and + tool calls are not failed merely because elapsed time is long. +- A failed model-backed check remains failed evidence even when an intermediate + report printed zero findings. Known third-party Strix/Pydantic serializer warnings must be filtered narrowly inside the Strix gate child process, not as a visible workflow env entry, so Warn-class logs are not accepted as clean evidence and warning-filter variable names do not pollute @@ -457,10 +445,10 @@ in this repo. update both backend header tests and frontend link tests. Global backend responses must include `Referrer-Policy`, and `target="_blank"` links must use explicit `rel="noopener noreferrer"`. -- When robot review cites an obsolete Strix provider policy, update the docs and - tests to the current GitHub Models default contract before accepting a - rollback suggestion; do not reintroduce generic `LLM_API_KEY` or - cross-provider credential forwarding while trying to satisfy old comments. +- When robot review cites obsolete provider-specific Strix policy, compare it + with the released `.github` and `contextual-orchestrator` contracts before + accepting a rollback suggestion. Do not reintroduce direct-provider routing, + generic provider credentials, or cross-provider credential forwarding. - When reviews find inert navigation/dead-space controls, either wire them to an implemented workspace route/API or remove the control; do not leave high-traffic drawer/sidebar entries as permanent `준비 중` copy. diff --git a/CLAUDE.md b/CLAUDE.md index 921f61cd1..0bfd4f580 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,7 +110,7 @@ indexes, and auditable writeback intent. `ARCHITECTURE.md` and ``` Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgvector │ - ├──> contextual-orchestrator released consumer contract + ├──> contextual-orchestrator consumer boundary │ └──> provider/model routing owned by contextual-orchestrator └──> outbound-only self-hosted connector (connector/) └──> customer IMAP/POP3/SMTP + CalDAV/CardDAV/WebDAV @@ -122,7 +122,9 @@ API/client/schema; a missing or incompatible owner contract fails closed instead of falling back to a direct provider. The Ollama/MLX paths in local Compose are explicit non-production development fixtures. Central model-backed GitHub Actions are owned by `ContextualWisdomLab/.github` and use `orchestrator/free`; do not copy -that pool selection into Naruon runtime. +that pool selection into Naruon runtime. Verify protected release evidence before +claiming the owner contract is available; an open PR or unreleased branch is only +proposed evidence. - `backend/` — FastAPI app (`main.py`, routers in `api/`, domain logic in `services/`, SQLAlchemy models in `db/`, Alembic in `alembic/`). Owns @@ -188,4 +190,4 @@ that pool selection into Naruon runtime. FastAPI app metadata synchronized; the backend reads its version from `VERSION`. - When a review uncovers a recurring bug pattern, record the anti-pattern in - `AGENTS.md` and update the affected tests/mocks/docs in the same PR. \ No newline at end of file + `AGENTS.md` and update the affected tests/mocks/docs in the same PR. diff --git a/backend/tests/test_agent_llm_authority_docs.py b/backend/tests/test_agent_llm_authority_docs.py index 8360e55a0..60624c80d 100644 --- a/backend/tests/test_agent_llm_authority_docs.py +++ b/backend/tests/test_agent_llm_authority_docs.py @@ -39,3 +39,8 @@ def test_agent_guidance_names_canonical_llm_owner_and_fail_closed_boundary() -> assert "contextual-orchestrator" in claude assert "Naruon owns" in claude assert "provider/model routing" in claude + assert "gateway token" in agents + assert "provider names, model" in agents + assert "immutable released owner API/client/schema" in agents + assert "open PR or unreleased branch" in agents + assert "shared application/agent/gateway wall-clock timeout" in agents From d1a0cbd582ee1667eea15983461c78da6312fc22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:45:15 +0900 Subject: [PATCH 10/31] test(governance): require OpenCode orchestrator boundary RED: make the repository OpenCode configuration fail if it selects GitHub Models or provider-specific model fallbacks instead of the canonical orchestrator/free logical pool. Assisted-by: OpenAI Codex Signed-off-by: Seongho Bae --- .../tests/test_agent_llm_authority_docs.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_agent_llm_authority_docs.py b/backend/tests/test_agent_llm_authority_docs.py index 60624c80d..07b60944a 100644 --- a/backend/tests/test_agent_llm_authority_docs.py +++ b/backend/tests/test_agent_llm_authority_docs.py @@ -1,5 +1,6 @@ -"""Regression contract for current LLM-routing authority guidance.""" +"""Regression contracts for current LLM-routing authority guidance.""" +import json from pathlib import Path @@ -44,3 +45,32 @@ def test_agent_guidance_names_canonical_llm_owner_and_fail_closed_boundary() -> assert "immutable released owner API/client/schema" in agents assert "open PR or unreleased branch" in agents assert "shared application/agent/gateway wall-clock timeout" in agents + + +def test_opencode_config_uses_only_contextual_orchestrator_free() -> None: + """Repository OpenCode model work must use only the canonical logical pool.""" + raw_config = _read("opencode.jsonc") + config = json.loads(raw_config) + + assert config["model"] == "contextual-orchestrator/orchestrator/free" + assert config["small_model"] == "contextual-orchestrator/orchestrator/free" + assert config["enabled_providers"] == ["contextual-orchestrator"] + assert set(config["provider"]) == {"contextual-orchestrator"} + + provider = config["provider"]["contextual-orchestrator"] + assert provider["options"] == { + "baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}", + "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}", + } + assert set(provider["models"]) == {"orchestrator/free"} + + forbidden_direct_routing = ( + "github-models", + "STRIX_GITHUB_MODELS_TOKEN", + "https://models.github.ai/inference", + '"openai/gpt-5"', + '"deepseek/deepseek-r1-0528"', + '"deepseek/deepseek-v3-0324"', + ) + for phrase in forbidden_direct_routing: + assert phrase not in raw_config From c249658908c8cf4dccc5122b47e1ab2a4e56474d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:45:29 +0900 Subject: [PATCH 11/31] fix(governance): route OpenCode through orchestrator free Replace repository-local GitHub Models selection and provider-specific fallbacks with the contextual-orchestrator gateway and logical orchestrator/free pool while preserving the existing MCP configuration. Assisted-by: OpenAI Codex Signed-off-by: Seongho Bae --- opencode.jsonc | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/opencode.jsonc b/opencode.jsonc index 536d212fb..f942535d1 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -1,8 +1,8 @@ { "$schema": "https://opencode.ai/config.json", - "model": "github-models/openai/gpt-5", - "small_model": "github-models/deepseek/deepseek-v3-0324", - "enabled_providers": ["github-models"], + "model": "contextual-orchestrator/orchestrator/free", + "small_model": "contextual-orchestrator/orchestrator/free", + "enabled_providers": ["contextual-orchestrator"], "mcp": { "codegraph": { "type": "local", @@ -37,38 +37,21 @@ } }, "provider": { - "github-models": { + "contextual-orchestrator": { "npm": "@ai-sdk/openai-compatible", - "name": "GitHub Models", + "name": "Contextual Orchestrator", "options": { - "baseURL": "https://models.github.ai/inference", - "apiKey": "{env:STRIX_GITHUB_MODELS_TOKEN}" + "baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}", + "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}" }, "models": { - "openai/gpt-5": { - "name": "OpenAI GPT-5", + "orchestrator/free": { + "name": "Orchestrator Free", "tool_call": true, "reasoning": true, "limit": { "context": 200000, - "output": 100000 - } - }, - "deepseek/deepseek-r1-0528": { - "name": "DeepSeek R1 0528", - "tool_call": true, - "reasoning": true, - "limit": { - "context": 128000, - "output": 4096 - } - }, - "deepseek/deepseek-v3-0324": { - "name": "DeepSeek V3 0324", - "tool_call": true, - "limit": { - "context": 128000, - "output": 4096 + "output": 32768 } } } From 57254cb61ecc67dd593ea96ae878bf651682a501 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 05:50:11 +0900 Subject: [PATCH 12/31] docs(agents): make repository entry verifiable Format the CWL entry as a readable source checklist and add a governance regression for the product spec, Project, gap baseline, completion issue, Figma proposal, and central operating documents.\n\nAssisted-by: OpenAI Codex --- AGENTS.md | 10 +++++++++- backend/tests/test_release_governance.py | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index ec36650bf..2f0a02442 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,15 @@ # AGENTS.md -> **Agents: read the master context FIRST.** Before any work, read [`docs/architecture/naruon-product-spec.md`](docs/architecture/naruon-product-spec.md) (product vision & architecture, RBAC/ABAC, core features & AI agent ontology, branding/UX, observability & governance), the live **GitHub Project #1** (work/roadmap source of truth), the live gap snapshot [`docs/product-technical-gap-baseline.md`](docs/product-technical-gap-baseline.md) (not merge authorization; canonical completion issue [#1428](https://github.com/ContextualWisdomLab/naruon/issues/1428); this repo's Figma file ID is not yet canonical on `develop` — the design-system decision (ADR-0013, file ID `68b5XB58w8nwT2LYOOnikK`) is still in open PR [#1436](https://github.com/ContextualWisdomLab/naruon/pull/1436)), and operate the Project per `ContextualWisdomLab/.github`'s [`docs/agent-github-project-protocol.md`](https://github.com/ContextualWisdomLab/.github/blob/main/docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth. The standing autonomous operating directive for the continuous PR review→fix→merge→develop loop across the ecosystem is `ContextualWisdomLab/.github`'s [`docs/product-goal-directive.md`](https://github.com/ContextualWisdomLab/.github/blob/main/docs/product-goal-directive.md) — a `/goal` session's length-capped pointer refers to it; read the full directive before running or configuring any such loop. +> **Agents: read the master context FIRST.** Before any work, read: +> +> - [`docs/architecture/naruon-product-spec.md`](docs/architecture/naruon-product-spec.md) for product vision, architecture, RBAC/ABAC, core features, AI agent ontology, branding/UX, observability, and governance; +> - the live [GitHub Project #1](https://github.com/orgs/ContextualWisdomLab/projects/1), the work and roadmap source of truth; +> - [`docs/product-technical-gap-baseline.md`](docs/product-technical-gap-baseline.md), the live gap snapshot but not merge authorization, and canonical completion issue [#1428](https://github.com/ContextualWisdomLab/naruon/issues/1428); +> - `ContextualWisdomLab/.github`'s [`docs/agent-github-project-protocol.md`](https://github.com/ContextualWisdomLab/.github/blob/main/docs/agent-github-project-protocol.md) for Project operation; and +> - `ContextualWisdomLab/.github`'s [`docs/product-goal-directive.md`](https://github.com/ContextualWisdomLab/.github/blob/main/docs/product-goal-directive.md) before running or configuring the continuous PR review→fix→merge→develop loop. A length-capped `/goal` pointer does not replace the full directive. +> +> This repository's Figma file ID is not yet canonical on `develop`: ADR-0013 and file ID `68b5XB58w8nwT2LYOOnikK` remain Proposed in open PR [#1436](https://github.com/ContextualWisdomLab/naruon/pull/1436). The repository and Project, not private agent memory, are authoritative. ## Agent guidance (CWL governance) diff --git a/backend/tests/test_release_governance.py b/backend/tests/test_release_governance.py index a23c70746..4d879f731 100644 --- a/backend/tests/test_release_governance.py +++ b/backend/tests/test_release_governance.py @@ -601,6 +601,20 @@ def test_scorecard_sarif_normalizer_rejects_escape_links_and_large_input( assert module.main([str(normalizer), str(expected)]) == 65 +def test_agents_entry_points_to_current_product_and_governance_sources() -> None: + agents = read_repo_text("AGENTS.md") + + assert "" in agents + assert "docs/architecture/naruon-product-spec.md" in agents + assert "docs/product-technical-gap-baseline.md" in agents + assert "ContextualWisdomLab/projects/1" in agents + assert "ContextualWisdomLab/naruon/issues/1428" in agents + assert "ContextualWisdomLab/naruon/pull/1436" in agents + assert "docs/agent-github-project-protocol.md" in agents + assert "docs/product-goal-directive.md" in agents + assert "not private agent memory" in agents + + def test_review_automation_uses_central_required_workflows_without_local_copies() -> ( None ): From f038377d7d95d445a0e9f3e37707278d792213a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 10:01:41 +0900 Subject: [PATCH 13/31] test(docs): pin architecture LLM owner boundary --- backend/tests/test_agent_llm_authority_docs.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_agent_llm_authority_docs.py b/backend/tests/test_agent_llm_authority_docs.py index 07b60944a..e8dcc9c40 100644 --- a/backend/tests/test_agent_llm_authority_docs.py +++ b/backend/tests/test_agent_llm_authority_docs.py @@ -13,17 +13,19 @@ def _read(path: str) -> str: def test_agent_guidance_does_not_reintroduce_direct_model_routing_authority() -> None: - """Current agent guidance must not prescribe direct provider/model routing.""" + """Current agent and architecture guidance must not prescribe direct routing.""" agents = _read("AGENTS.md") claude = _read("CLAUDE.md") + architecture = _read("ARCHITECTURE.md") forbidden_current_guidance = ( "STRIX_GITHUB_MODELS_TOKEN", "https://models.github.ai/inference", "Direct OpenAI GPT-5.4-or-newer", "OpenAI-compatible LLM providers", + "API --> LLM[OpenAI APIs when configured]", ) - combined = f"{agents}\n{claude}" + combined = f"{agents}\n{claude}\n{architecture}" for phrase in forbidden_current_guidance: assert phrase not in combined @@ -32,6 +34,7 @@ def test_agent_guidance_names_canonical_llm_owner_and_fail_closed_boundary() -> """Guidance must preserve product ownership while delegating LLM routing.""" agents = _read("AGENTS.md") claude = _read("CLAUDE.md") + architecture = _read("ARCHITECTURE.md") assert "ContextualWisdomLab/.github" in agents assert "contextual-orchestrator" in agents @@ -45,6 +48,10 @@ def test_agent_guidance_names_canonical_llm_owner_and_fail_closed_boundary() -> assert "immutable released owner API/client/schema" in agents assert "open PR or unreleased branch" in agents assert "shared application/agent/gateway wall-clock timeout" in agents + assert "contextual-orchestrator" in architecture + assert "provider discovery" in architecture + assert "immutable released" in architecture + assert "fails closed" in architecture def test_opencode_config_uses_only_contextual_orchestrator_free() -> None: From 0907cb1b2cfed408411953bd6545c0ff00e41d82 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 21:18:52 +0900 Subject: [PATCH 14/31] docs(architecture): align LLM owner boundary --- ARCHITECTURE.md | 67 ++++++++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9d2cbba18..0ad757b07 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -6,7 +6,7 @@ flowchart LR UI[Next.js frontend] --> API[FastAPI backend / Naruon control plane] API --> DB[(Postgres + pgvector)] - API --> LLM[OpenAI APIs when configured] + API --> LLM[contextual-orchestrator released consumer API] API --> CONN[Outbound-only self-hosted connector] CONN --> MAIL[Customer IMAP/POP3/SMTP] CONN --> DAV[Customer CalDAV/CardDAV/WebDAV] @@ -20,6 +20,15 @@ Runtime database connectivity is secret-injected: `backend/core/config.py` has no fallback `DATABASE_URL`, so missing database configuration fails at startup rather than silently using shared development credentials. +Production LLM work crosses an immutable released `contextual-orchestrator` +API/client/schema. `contextual-orchestrator` owns provider discovery, +capability-based routing, free-pool membership, and provider fallback; Naruon +owns product/domain truth, authorization, tools, and context assembly. A missing +or incompatible released owner contract fails closed instead of creating a +Naruon-local provider/model fallback. Local deterministic fixtures and legacy +provider surfaces may remain for migration/testing only when they are explicitly +identified as non-production authority. + ## Topic-intelligence boundary Naruon has no live Structural Topic Modeling endpoint, fitted topic artifact, @@ -83,7 +92,10 @@ registry/list/update/delete plus prompt-preview provider selection paths filter the authenticated organization. Existing local databases get the same fail-closed owner backfill through `scripts/bootstrap_db.py`; legacy provider rows require explicit non-default `NARUON_IMPORT_USER_ID` and `NARUON_IMPORT_ORGANIZATION_ID` -before bootstrap will set the new columns non-null. +before bootstrap will set the new columns non-null. This table and its direct +provider-routing fields are legacy migration surfaces, not the production +provider/model authority; new production LLM work must consume the released +`contextual-orchestrator` contract instead of extending this registry as a router. `ticket_tasks` stores email-derived execution items as ticket-like work records. The table and its new columns use at least two-word `snake_case` database names @@ -170,13 +182,15 @@ ETag/If-Match-guarded CalDAV/WebDAV PUT local adapter handlers for `SelfHostedConnector`; packaging, registration-token lifecycle, backend dispatch, and non-DAV protocol adapters remain separate connector delivery work. -LLM provider `base_url` is also a server-side egress boundary, not an arbitrary -URL field. Provider registry create/update paths are organization-admin scoped, -and LLM call sinks validate custom OpenAI-compatible base URLs with HTTPS-only -syntax, no userinfo/query or fragment, exact host membership in -`ALLOWED_LLM_BASE_URL_HOSTS`, and DNS answers that are all globally routable. -Missing allowlist configuration fails closed; the default provider path should -leave `base_url` unset. +The legacy LLM-provider `base_url` path remains a server-side egress boundary +while migration is incomplete, not an arbitrary URL field or production routing +authority. Existing provider registry create/update paths remain +organization-admin scoped, and current call sinks retain HTTPS-only syntax, +userinfo/query/fragment rejection, exact `ALLOWED_LLM_BASE_URL_HOSTS` membership, +and globally routable DNS validation until the legacy path is removed. Missing +allowlist configuration still fails closed. New production model work must not +select a provider endpoint through this surface; it uses the immutable released +`contextual-orchestrator` consumer contract. ## Batch embedding routing boundary @@ -192,12 +206,13 @@ SSRF-guarded, allowlisted, pinned-address HTTP client (`build_llm_provider_http_client`) that fronts every other outbound LLM call. naruon records a durable `llm_batch_jobs` / `llm_batch_items` audit trail (routing mode, the orchestrator's batch id, reported cost) for observability. -The path degrades gracefully: if batching is disabled, the orchestrator base URL -is rejected by the egress guard, or the orchestrator is unreachable, the caller -transparently falls back to the per-item embedding path. A local `pg-llm-batch` -package/checkout remains only as an optional offline-dev fallback, gated behind -orchestrator-unavailable and an explicit local DSN; naruon does not vendor a -gitlink for that fallback in this PR. +The currently implemented per-item fallback is migration behavior rather than +canonical provider authority: if batching is disabled or the orchestrator path +cannot be used, callers may still reach the legacy per-item embedding path, but +that path must not grow new production provider/model selection. A local +`pg-llm-batch` package/checkout remains only as an optional offline-dev fallback, +gated behind orchestrator-unavailable and an explicit local DSN; naruon does not +vendor a gitlink for that fallback in this PR. ## Semantic project-graph extractor seam @@ -216,16 +231,16 @@ and the runner degrades down the chain instead of losing the projection. New extractors (including future plugins on the platform plan's `kg.extractor` extension point) register a selector without editing ingest. -Routing LLM extraction through **contextual-orchestrator** is modelled as a -transport concern: the orchestrator is an OpenAI-compatible gateway, so the -`orchestrator` selector reuses the identical grounded LLM extractor -(`extract_project_semantics_llm`, which enforces segment citations) but points -its SSRF-allowlisted client (`build_llm_provider_http_client`) at -`PROJECT_GRAPH_ORCHESTRATOR_BASE_URL` instead of the raw provider. The provider -API key stays the tenant's Fernet-encrypted credential, and the orchestrator base -URL must be HTTPS and exact-host allowlisted by `ALLOWED_LLM_BASE_URL_HOSTS`; -an unset or rejected endpoint fails closed to the deterministic extractor. Design -and grounding: [`docs/architecture/kg-extractor-seam.md`](docs/architecture/kg-extractor-seam.md). +The current `orchestrator` selector still reuses the grounded LLM extractor and +an OpenAI-compatible transport adapter, but that compatibility layer is legacy +migration plumbing rather than Naruon-owned provider/model routing. It points the +SSRF-allowlisted client (`build_llm_provider_http_client`) at +`PROJECT_GRAPH_ORCHESTRATOR_BASE_URL`; its existing credential and allowlist +handling remains in place until the released owner client replaces it. New +production behavior must consume the immutable released `contextual-orchestrator` +API/client/schema and must not add raw-provider fallback here. An unset or +rejected endpoint fails closed to the deterministic extractor. Design and +grounding: [`docs/architecture/kg-extractor-seam.md`](docs/architecture/kg-extractor-seam.md). ## CI security boundary @@ -319,4 +334,4 @@ target source id, but it must not provide source ownership or capability records `backend/api/calendar.py` obtains writeback sources through a FastAPI dependency that reads DB-backed `calendar_writeback_sources` records scoped to the authenticated user. Provider execution remains opt-in and fail-closed without an -active outbound runner or If-Match/ETag evidence. +active outbound runner or If-Match/ETag evidence. \ No newline at end of file From 1261c32c6287209bf97ccaa6a77f728fed0f54a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 14:28:58 +0900 Subject: [PATCH 15/31] docs(agents): preserve verified smoke and delivery know-how Restore the complete AGENTS delta removed from PR #1599 into the existing documentation owner lane #1549. Require exact successor evidence before relocating guidance. Co-Authored-By: Codex Signed-off-by: Seongho Bae --- AGENTS.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 4a66b17d9..fba8c0bc5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -273,6 +273,15 @@ in this repo. ## Workspace and task tracking defaults +- Browser smoke must check collected console and page errors after interaction, + accessibility, screenshot capture, and page cleanup, not only after navigation. + Keep the early check for fast failure and a final check before returning success. + Regression tests must inject late errors into the actual route-smoke execution; + a successful screenshot or a passing render assertion does not prove clean interactions. + If screenshot retries are exhausted, throw the capture failure; do not create + diagnostic text files in the temporary capture directory or count them as screenshots. + Close the page in a finally block, including navigation and capture failures. + - First-run frontend sessions should open the Today execution dashboard while preserving explicit Dashboard, Email, and Calendar startup choices. - Workspace navigation changes must keep the desktop primary nav and the @@ -648,6 +657,28 @@ in this repo. ## Development environment and tooling defaults +- Read the applicable repository skill before changing its contract: + [fix-development-mistakes](.agents/skills/fix-development-mistakes/SKILL.md) + for failures and security findings, + [github-actions-privileged-pr-scan](.agents/skills/github-actions-privileged-pr-scan/SKILL.md) + for privileged PR scanners, and + [github-robot-review-gate](.agents/skills/github-robot-review-gate/SKILL.md) + for check/review diagnosis. Record the failing reproduction, smallest causal + repair, exact verification command, and remaining gates in the existing PR. +- Visual Inspection requires opening the actual rendered pages or captured + images, not merely counting PNG files. Record app/build SHA separately from + runner SHA, viewport, routes, and observed defects. Mocked browser evidence + does not prove live provider behavior, all locales, or deployment readiness. + Inspect the changed AGENTS.md rendering on the pushed revision as well. +- Preserve concurrent commits with ordinary history integration, then verify + the combined tree before pushing. Update these instructions with reusable + failure-prevention lessons; keep dated findings in the gap baseline/PR. +- Package registry credentials alone do not authorize or configure a cluster + deployment. Use the existing release workflow only after its protected-source, + checks, target credentials, and destination prerequisites are verified; report + package publication and live deployment as separate outcomes. +- Before relocating guidance out of a PR, verify its complete successor delta + and record the destination PR, commit, and path; a named owner alone is not succession. - If CodeGraph is not initialized for this repository, agents may run `codegraph init -i` autonomously without asking first; keep generated `.codegraph/` and `.cursor/rules/codegraph.mdc` artifacts local unless a From c54c005a6802a47ff59ab3058d82eed3fcca42a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 15:05:48 +0900 Subject: [PATCH 16/31] docs(agents): record smoke API and startup evidence boundaries Signed-off-by: Seongho Bae Co-authored-by: Codex --- AGENTS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index fba8c0bc5..1ccdd17ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -281,6 +281,10 @@ in this repo. If screenshot retries are exhausted, throw the capture failure; do not create diagnostic text files in the temporary capture directory or count them as screenshots. Close the page in a finally block, including navigation and capture failures. +- Browser-smoke API mocks must reject unregistered requests with a non-success + response and record a harness failure even when the UI suppresses the error. + Cover requests emitted during cleanup; never return a default `{ ok: true }`. + Keep request URLs, query values, and payloads out of failure diagnostics. - First-run frontend sessions should open the Today execution dashboard while preserving explicit Dashboard, Email, and Calendar startup choices. @@ -670,6 +674,9 @@ in this repo. runner SHA, viewport, routes, and observed defects. Mocked browser evidence does not prove live provider behavior, all locales, or deployment readiness. Inspect the changed AGENTS.md rendering on the pushed revision as well. +- A server-ready timeout before browser navigation is failed startup evidence, + not a completed visual inspection. Record the exact source and execution result; + host load alone does not prove the cause or justify increasing test timeouts. - Preserve concurrent commits with ordinary history integration, then verify the combined tree before pushing. Update these instructions with reusable failure-prevention lessons; keep dated findings in the gap baseline/PR. From a3421f3e9dbfe3a42fc8812cc31b1c6fafb1150b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:50:41 +0900 Subject: [PATCH 17/31] fix(deps): bump next to 16.3.4 and sharp override to 0.35.4 --- frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 370 +++++++++++++++++++---------------- frontend/pnpm-workspace.yaml | 2 +- 3 files changed, 199 insertions(+), 177 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 191b7c90b..c86342aa1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.27.0", - "next": "16.2.12", + "next": "16.3.4", "react": "19.2.8", "react-dom": "19.2.8", "react-resizable-panels": "^4.12.2", @@ -39,7 +39,7 @@ "@types/react-dom": "^19", "@vitest/coverage-v8": "4.1.10", "eslint": "^9", - "eslint-config-next": "16.2.12", + "eslint-config-next": "16.3.4", "fast-check": "^4.9.0", "jsdom": "^30.0.1", "postcss": "8.5.24", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 610a0e7ca..add535314 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: overrides: brace-expansion: 5.0.9 postcss: 8.5.24 - sharp: 0.35.0 + sharp: 0.35.4 undici: 8.9.0 pnpmfileChecksum: sha256-RXPq3MmEdRb3xD3rhbER9kciz9nBr/i0J/uMUjql5t0= @@ -38,8 +38,8 @@ importers: specifier: ^1.27.0 version: 1.27.0(react@19.2.8) next: - specifier: 16.2.12 - version: 16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 16.3.4 + version: 16.3.4(@babel/core@7.29.7)(@playwright/test@1.62.0)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: specifier: 19.2.8 version: 19.2.8 @@ -84,8 +84,8 @@ importers: specifier: ^9 version: 9.39.5(jiti@2.7.0) eslint-config-next: - specifier: 16.2.12 - version: 16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + specifier: 16.3.4 + version: 16.3.4(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) fast-check: specifier: ^4.9.0 version: 4.9.0 @@ -289,6 +289,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -369,160 +375,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.0': - resolution: {integrity: sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.0': - resolution: {integrity: sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.0': - resolution: {integrity: sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.0': - resolution: {integrity: sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.0': - resolution: {integrity: sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.0': - resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.0': - resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.0': - resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.0': - resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.0': - resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.0': - resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': - resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.0': - resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.0': - resolution: {integrity: sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.0': - resolution: {integrity: sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.0': - resolution: {integrity: sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.0': - resolution: {integrity: sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.0': - resolution: {integrity: sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.0': - resolution: {integrity: sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.0': - resolution: {integrity: sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.0': - resolution: {integrity: sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.0': - resolution: {integrity: sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.0': - resolution: {integrity: sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.0': - resolution: {integrity: sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.0': - resolution: {integrity: sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.0': - resolution: {integrity: sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -549,60 +555,60 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.12': - resolution: {integrity: sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==} + '@next/env@16.3.4': + resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} - '@next/eslint-plugin-next@16.2.12': - resolution: {integrity: sha512-uF2z/qAK2q7B5/6CpnFcBRX6jOq5iCO+Uqh1UkJhXljX1JwLarLYhhoJadO6dPb6moTprOKewMXheBcbIoSbug==} + '@next/eslint-plugin-next@16.3.4': + resolution: {integrity: sha512-szW9y2Aumu4z88YXfTzcFsgUAg2k64uzbtcO5L9f1AKS4w/GUKJcbFllRflROVyNPgJtGOnvNxiyp3v6b+prIA==} - '@next/swc-darwin-arm64@16.2.12': - resolution: {integrity: sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==} + '@next/swc-darwin-arm64@16.3.4': + resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.12': - resolution: {integrity: sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==} + '@next/swc-darwin-x64@16.3.4': + resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.12': - resolution: {integrity: sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==} + '@next/swc-linux-arm64-gnu@16.3.4': + resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.12': - resolution: {integrity: sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==} + '@next/swc-linux-arm64-musl@16.3.4': + resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.12': - resolution: {integrity: sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==} + '@next/swc-linux-x64-gnu@16.3.4': + resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.12': - resolution: {integrity: sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==} + '@next/swc-linux-x64-musl@16.3.4': + resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.12': - resolution: {integrity: sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==} + '@next/swc-win32-arm64-msvc@16.3.4': + resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.12': - resolution: {integrity: sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==} + '@next/swc-win32-x64-msvc@16.3.4': + resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -893,8 +899,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -1529,8 +1535,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@16.2.12: - resolution: {integrity: sha512-iaaf4vvKo5h2LBdGt0JuRv7t0Ysqr9FMCiFxbptDg8LqOE//mIKR80DdpOnSVM7qjLH3jT8P0aFiwXxBEGZRXw==} + eslint-config-next@16.3.4: + resolution: {integrity: sha512-35/8RM10huEL9vlr8hUZMERMENHBrnyHN3ZZkF9efSgzGaqK34jIqry44A956//zriUhUAUW0XSkcolhrryqAA==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -1622,6 +1628,7 @@ packages: eslint@9.39.5: resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -2259,8 +2266,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@16.2.12: - resolution: {integrity: sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==} + next@16.3.4: + resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -2504,9 +2511,14 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - sharp@0.35.0: - resolution: {integrity: sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -3130,6 +3142,11 @@ snapshots: eslint: 9.39.5(jiti@2.7.0) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@2.7.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.2': {} '@eslint/config-array@0.21.2': @@ -3209,108 +3226,108 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.35.0': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.0 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.35.0': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.0 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.0': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.0 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.0': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.0': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.0': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.0': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.0': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.0': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.0': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.0': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.0': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.35.0': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.0 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.35.0': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.0 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.35.0': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.0 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.35.0': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.0 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.35.0': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.0 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.35.0': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.0 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.0': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.0': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.35.0': + '@img/sharp-wasm32@0.35.4': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.0': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.0 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.35.0': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.35.0': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.35.0': + '@img/sharp-win32-x64@0.35.4': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -3346,34 +3363,37 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.2.12': {} + '@next/env@16.3.4': {} - '@next/eslint-plugin-next@16.2.12': + '@next/eslint-plugin-next@16.3.4(eslint@9.39.5(jiti@2.7.0))': dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)) fast-glob: 3.3.1 + transitivePeerDependencies: + - eslint - '@next/swc-darwin-arm64@16.2.12': + '@next/swc-darwin-arm64@16.3.4': optional: true - '@next/swc-darwin-x64@16.2.12': + '@next/swc-darwin-x64@16.3.4': optional: true - '@next/swc-linux-arm64-gnu@16.2.12': + '@next/swc-linux-arm64-gnu@16.3.4': optional: true - '@next/swc-linux-arm64-musl@16.2.12': + '@next/swc-linux-arm64-musl@16.3.4': optional: true - '@next/swc-linux-x64-gnu@16.2.12': + '@next/swc-linux-x64-gnu@16.3.4': optional: true - '@next/swc-linux-x64-musl@16.2.12': + '@next/swc-linux-x64-musl@16.3.4': optional: true - '@next/swc-win32-arm64-msvc@16.2.12': + '@next/swc-win32-arm64-msvc@16.3.4': optional: true - '@next/swc-win32-x64-msvc@16.2.12': + '@next/swc-win32-x64-msvc@16.3.4': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3584,7 +3604,7 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.15': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -4287,9 +4307,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3): + eslint-config-next@16.3.4(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@next/eslint-plugin-next': 16.2.12 + '@next/eslint-plugin-next': 16.3.4(eslint@9.39.5(jiti@2.7.0)) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) @@ -5055,10 +5075,10 @@ snapshots: natural-compare@1.4.0: {} - next@16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.4(@babel/core@7.29.7)(@playwright/test@1.62.0)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.2.12 - '@swc/helpers': 0.5.15 + '@next/env': 16.3.4 + '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.11.5 caniuse-lite: 1.0.30001806 postcss: 8.5.24 @@ -5066,18 +5086,19 @@ snapshots: react-dom: 19.2.8(react@19.2.8) styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.8) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.12 - '@next/swc-darwin-x64': 16.2.12 - '@next/swc-linux-arm64-gnu': 16.2.12 - '@next/swc-linux-arm64-musl': 16.2.12 - '@next/swc-linux-x64-gnu': 16.2.12 - '@next/swc-linux-x64-musl': 16.2.12 - '@next/swc-win32-arm64-msvc': 16.2.12 - '@next/swc-win32-x64-msvc': 16.2.12 + '@next/swc-darwin-arm64': 16.3.4 + '@next/swc-darwin-x64': 16.3.4 + '@next/swc-linux-arm64-gnu': 16.3.4 + '@next/swc-linux-arm64-musl': 16.3.4 + '@next/swc-linux-x64-gnu': 16.3.4 + '@next/swc-linux-x64-musl': 16.3.4 + '@next/swc-win32-arm64-msvc': 16.3.4 + '@next/swc-win32-x64-msvc': 16.3.4 '@playwright/test': 1.62.0 - sharp: 0.35.0 + sharp: 0.35.4(@types/node@26.1.2) transitivePeerDependencies: - '@babel/core' + - '@types/node' - babel-plugin-macros node-exports-info@1.6.2: @@ -5338,37 +5359,38 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 - sharp@0.35.0: + sharp@0.35.4(@types/node@26.1.2): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.0 - '@img/sharp-darwin-x64': 0.35.0 - '@img/sharp-freebsd-wasm32': 0.35.0 - '@img/sharp-libvips-darwin-arm64': 1.3.0 - '@img/sharp-libvips-darwin-x64': 1.3.0 - '@img/sharp-libvips-linux-arm': 1.3.0 - '@img/sharp-libvips-linux-arm64': 1.3.0 - '@img/sharp-libvips-linux-ppc64': 1.3.0 - '@img/sharp-libvips-linux-riscv64': 1.3.0 - '@img/sharp-libvips-linux-s390x': 1.3.0 - '@img/sharp-libvips-linux-x64': 1.3.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 - '@img/sharp-linux-arm': 0.35.0 - '@img/sharp-linux-arm64': 0.35.0 - '@img/sharp-linux-ppc64': 0.35.0 - '@img/sharp-linux-riscv64': 0.35.0 - '@img/sharp-linux-s390x': 0.35.0 - '@img/sharp-linux-x64': 0.35.0 - '@img/sharp-linuxmusl-arm64': 0.35.0 - '@img/sharp-linuxmusl-x64': 0.35.0 - '@img/sharp-webcontainers-wasm32': 0.35.0 - '@img/sharp-win32-arm64': 0.35.0 - '@img/sharp-win32-ia32': 0.35.0 - '@img/sharp-win32-x64': 0.35.0 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 26.1.2 optional: true shebang-command@2.0.0: diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index d028031d2..784b361c1 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -16,7 +16,7 @@ supportedArchitectures: overrides: brace-expansion: "5.0.9" postcss: "8.5.24" - sharp: "0.35.0" + sharp: "0.35.4" undici: 8.9.0 patchedDependencies: From d76f7b450888d42040ca090ed9b9924e08f01881 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:56:07 +0900 Subject: [PATCH 18/31] test(security): lock frontend patched dependency floors --- .../test_frontend_framework_security_floor.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 backend/tests/test_frontend_framework_security_floor.py diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py new file mode 100644 index 000000000..4130e8e7a --- /dev/null +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -0,0 +1,52 @@ +"""Fail closed when frontend framework/image dependencies regress below patched floors.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +FRONTEND_ROOT = REPO_ROOT / "frontend" + + +def _exact_version(value: str) -> tuple[int, int, int]: + """Return a three-part exact version, rejecting ranges and prereleases.""" + + match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", value) + assert match is not None, f"expected exact semantic version, got {value!r}" + return tuple(int(part) for part in match.groups()) + + +def test_frontend_framework_and_image_security_floors() -> None: + """Keep Next.js and sharp at releases containing the reviewed security fixes.""" + + package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) + next_value = package["dependencies"]["next"] + eslint_next_value = package["devDependencies"]["eslint-config-next"] + + assert _exact_version(next_value) >= (16, 3, 3), ( + "Next.js must include the fixes for CVE-2026-75604 and " + "GHSA-2xp9-vwfh-vxw4" + ) + assert eslint_next_value == next_value, ( + "eslint-config-next must stay on the same reviewed release as Next.js" + ) + + workspace = (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + sharp_match = re.search( + r"(?m)^\s{2}sharp:\s*[\"']?(\d+\.\d+\.\d+)[\"']?\s*$", + workspace, + ) + assert sharp_match is not None, "pnpm workspace must keep an explicit sharp override" + sharp_value = sharp_match.group(1) + assert _exact_version(sharp_value) >= (0, 35, 4), ( + "sharp must include the fix for GHSA-rgj7-g3m4-5g8c" + ) + + lock = (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + assert f"next@{next_value}" in lock, "lockfile must resolve the reviewed Next.js release" + assert f"sharp@{sharp_value}" in lock, "lockfile must resolve the reviewed sharp release" + assert "next@16.3.1" not in lock, "vulnerable Next.js 16.3.1 must not remain locked" + assert "sharp@0.35.0" not in lock, "vulnerable sharp 0.35.0 must not remain locked" From a5abe047d318477b64c62cdbee5b737aad8054f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 12:43:03 +0900 Subject: [PATCH 19/31] test(deps): reproduce weak frontend lock validation --- .../test_frontend_framework_security_floor.py | 79 ++++++++++++++++--- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 4130e8e7a..19ce8f7f2 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -6,6 +6,9 @@ import re from pathlib import Path +import pytest +import yaml + REPO_ROOT = Path(__file__).resolve().parents[2] FRONTEND_ROOT = REPO_ROOT / "frontend" @@ -19,6 +22,25 @@ def _exact_version(value: str) -> tuple[int, int, int]: return tuple(int(part) for part in match.groups()) +def _assert_weak_lock_text_contract( + lock_text: str, next_value: str, sharp_value: str +) -> None: + """Preserve the predecessor lock checks while stronger regressions are RED.""" + + assert f"next@{next_value}" in lock_text, ( + "lockfile must resolve the reviewed Next.js release" + ) + assert f"sharp@{sharp_value}" in lock_text, ( + "lockfile must resolve the reviewed sharp release" + ) + assert "next@16.3.1" not in lock_text, ( + "vulnerable Next.js 16.3.1 must not remain locked" + ) + assert "sharp@0.35.0" not in lock_text, ( + "vulnerable sharp 0.35.0 must not remain locked" + ) + + def test_frontend_framework_and_image_security_floors() -> None: """Keep Next.js and sharp at releases containing the reviewed security fixes.""" @@ -34,19 +56,54 @@ def test_frontend_framework_and_image_security_floors() -> None: "eslint-config-next must stay on the same reviewed release as Next.js" ) - workspace = (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - sharp_match = re.search( - r"(?m)^\s{2}sharp:\s*[\"']?(\d+\.\d+\.\d+)[\"']?\s*$", - workspace, + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") ) - assert sharp_match is not None, "pnpm workspace must keep an explicit sharp override" - sharp_value = sharp_match.group(1) + sharp_value = str(workspace["overrides"]["sharp"]) assert _exact_version(sharp_value) >= (0, 35, 4), ( "sharp must include the fix for GHSA-rgj7-g3m4-5g8c" ) - lock = (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - assert f"next@{next_value}" in lock, "lockfile must resolve the reviewed Next.js release" - assert f"sharp@{sharp_value}" in lock, "lockfile must resolve the reviewed sharp release" - assert "next@16.3.1" not in lock, "vulnerable Next.js 16.3.1 must not remain locked" - assert "sharp@0.35.0" not in lock, "vulnerable sharp 0.35.0 must not remain locked" + lock_text = (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + _assert_weak_lock_text_contract(lock_text, next_value, sharp_value) + + +def test_security_floor_rejects_importer_drift() -> None: + """Reject a partially regenerated lock whose root importer drifts below the floor.""" + + package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) + next_value = package["dependencies"]["next"] + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + ) + sharp_value = str(workspace["overrides"]["sharp"]) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + lock["importers"]["."]["dependencies"]["next"]["specifier"] = "16.3.2" + + with pytest.raises(AssertionError): + _assert_weak_lock_text_contract( + yaml.safe_dump(lock, sort_keys=False), next_value, sharp_value + ) + + +def test_security_floor_rejects_any_below_floor_lock_entry() -> None: + """Reject stale vulnerable package/snapshot entries, not only two known literals.""" + + package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) + next_value = package["dependencies"]["next"] + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + ) + sharp_value = str(workspace["overrides"]["sharp"]) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + lock["packages"]["next@16.3.2"] = {} + lock["snapshots"]["sharp@0.35.3"] = {} + + with pytest.raises(AssertionError): + _assert_weak_lock_text_contract( + yaml.safe_dump(lock, sort_keys=False), next_value, sharp_value + ) From e8a54fc5156ac3ffbb79bc8418c5125d7dcdea60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 12:43:37 +0900 Subject: [PATCH 20/31] fix(deps): validate frontend lock security floors structurally --- .../test_frontend_framework_security_floor.py | 165 ++++++++++++------ 1 file changed, 111 insertions(+), 54 deletions(-) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 19ce8f7f2..bf318b5ad 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -5,6 +5,7 @@ import json import re from pathlib import Path +from typing import Any import pytest import yaml @@ -12,6 +13,8 @@ REPO_ROOT = Path(__file__).resolve().parents[2] FRONTEND_ROOT = REPO_ROOT / "frontend" +NEXT_SECURITY_FLOOR = (16, 3, 3) +SHARP_SECURITY_FLOOR = (0, 35, 4) def _exact_version(value: str) -> tuple[int, int, int]: @@ -22,88 +25,142 @@ def _exact_version(value: str) -> tuple[int, int, int]: return tuple(int(part) for part in match.groups()) -def _assert_weak_lock_text_contract( - lock_text: str, next_value: str, sharp_value: str +def _resolved_version(value: str) -> tuple[int, int, int]: + """Return the exact version prefix from a pnpm peer-qualified resolution.""" + + version = value.split("(", 1)[0] + return _exact_version(version) + + +def _package_key_version(package_key: str, package_name: str) -> tuple[int, int, int]: + """Return the version encoded by one pnpm package/snapshot key.""" + + prefix = f"{package_name}@" + assert package_key.startswith(prefix), ( + f"expected {package_name!r} lock key, got {package_key!r}" + ) + return _resolved_version(package_key[len(prefix) :]) + + +def _assert_lock_contract( + lock: dict[str, Any], + next_value: str, + eslint_next_value: str, + sharp_value: str, ) -> None: - """Preserve the predecessor lock checks while stronger regressions are RED.""" + """Validate root resolution identity and every locked Next.js/sharp security floor.""" - assert f"next@{next_value}" in lock_text, ( - "lockfile must resolve the reviewed Next.js release" + importer = lock["importers"]["."] + next_import = importer["dependencies"]["next"] + assert next_import["specifier"] == next_value, ( + "root importer must preserve the package.json Next.js specifier" + ) + assert _resolved_version(str(next_import["version"])) == _exact_version(next_value), ( + "root importer must resolve the reviewed Next.js release" ) - assert f"sharp@{sharp_value}" in lock_text, ( - "lockfile must resolve the reviewed sharp release" + assert f"next@{next_import['version']}" in lock["snapshots"], ( + "root importer Next.js resolution must reference an existing snapshot" ) - assert "next@16.3.1" not in lock_text, ( - "vulnerable Next.js 16.3.1 must not remain locked" + + eslint_next_import = importer["devDependencies"]["eslint-config-next"] + assert eslint_next_import["specifier"] == eslint_next_value, ( + "root importer must preserve the eslint-config-next specifier" ) - assert "sharp@0.35.0" not in lock_text, ( - "vulnerable sharp 0.35.0 must not remain locked" + assert _resolved_version(str(eslint_next_import["version"])) == _exact_version( + eslint_next_value + ), "root importer must resolve the reviewed eslint-config-next release" + assert f"eslint-config-next@{eslint_next_import['version']}" in lock["snapshots"], ( + "root importer eslint-config-next resolution must reference an existing snapshot" ) + assert str(lock["overrides"]["sharp"]) == sharp_value, ( + "lockfile sharp override must match the reviewed workspace override" + ) -def test_frontend_framework_and_image_security_floors() -> None: - """Keep Next.js and sharp at releases containing the reviewed security fixes.""" + expected_next = _exact_version(next_value) + expected_sharp = _exact_version(sharp_value) + for section_name in ("packages", "snapshots"): + section = lock[section_name] + next_keys = [key for key in section if key.startswith("next@")] + sharp_keys = [key for key in section if key.startswith("sharp@")] + + assert next_keys, f"{section_name} must contain a Next.js resolution" + assert sharp_keys, f"{section_name} must contain a sharp resolution" + assert any( + _package_key_version(key, "next") == expected_next for key in next_keys + ), f"{section_name} must contain the reviewed Next.js release" + assert any( + _package_key_version(key, "sharp") == expected_sharp for key in sharp_keys + ), f"{section_name} must contain the reviewed sharp release" + + for package_key in next_keys: + assert _package_key_version(package_key, "next") >= NEXT_SECURITY_FLOOR, ( + f"{section_name} contains Next.js below the reviewed security floor: " + f"{package_key}" + ) + for package_key in sharp_keys: + assert _package_key_version(package_key, "sharp") >= SHARP_SECURITY_FLOOR, ( + f"{section_name} contains sharp below the reviewed security floor: " + f"{package_key}" + ) + + +def _frontend_security_inputs() -> tuple[str, str, str, dict[str, Any]]: + """Load the manifest, workspace override, and generated lock contract.""" package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) next_value = package["dependencies"]["next"] eslint_next_value = package["devDependencies"]["eslint-config-next"] + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + ) + sharp_value = str(workspace["overrides"]["sharp"]) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + return next_value, eslint_next_value, sharp_value, lock - assert _exact_version(next_value) >= (16, 3, 3), ( + +def test_frontend_framework_and_image_security_floors() -> None: + """Keep manifests and every generated lock resolution at reviewed patched releases.""" + + next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() + + assert _exact_version(next_value) >= NEXT_SECURITY_FLOOR, ( "Next.js must include the fixes for CVE-2026-75604 and " "GHSA-2xp9-vwfh-vxw4" ) assert eslint_next_value == next_value, ( "eslint-config-next must stay on the same reviewed release as Next.js" ) - - workspace = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - ) - sharp_value = str(workspace["overrides"]["sharp"]) - assert _exact_version(sharp_value) >= (0, 35, 4), ( + assert _exact_version(sharp_value) >= SHARP_SECURITY_FLOOR, ( "sharp must include the fix for GHSA-rgj7-g3m4-5g8c" ) + _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) - lock_text = (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - _assert_weak_lock_text_contract(lock_text, next_value, sharp_value) +@pytest.mark.parametrize("field", ["specifier", "version"]) +def test_security_floor_rejects_root_importer_drift(field: str) -> None: + """Reject a partially regenerated lock whose root Next.js importer drifts.""" -def test_security_floor_rejects_importer_drift() -> None: - """Reject a partially regenerated lock whose root importer drifts below the floor.""" - - package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) - next_value = package["dependencies"]["next"] - workspace = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - ) - sharp_value = str(workspace["overrides"]["sharp"]) - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - lock["importers"]["."]["dependencies"]["next"]["specifier"] = "16.3.2" + next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() + lock["importers"]["."]["dependencies"]["next"][field] = "16.3.2" with pytest.raises(AssertionError): - _assert_weak_lock_text_contract( - yaml.safe_dump(lock, sort_keys=False), next_value, sharp_value - ) + _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) -def test_security_floor_rejects_any_below_floor_lock_entry() -> None: - """Reject stale vulnerable package/snapshot entries, not only two known literals.""" +@pytest.mark.parametrize( + ("section_name", "package_key"), + [("packages", "next@16.3.2"), ("snapshots", "sharp@0.35.3")], +) +def test_security_floor_rejects_every_below_floor_lock_entry( + section_name: str, package_key: str +) -> None: + """Reject any stale vulnerable Next.js or sharp package/snapshot entry.""" - package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) - next_value = package["dependencies"]["next"] - workspace = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") - ) - sharp_value = str(workspace["overrides"]["sharp"]) - lock = yaml.safe_load( - (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") - ) - lock["packages"]["next@16.3.2"] = {} - lock["snapshots"]["sharp@0.35.3"] = {} + next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() + lock[section_name][package_key] = {} with pytest.raises(AssertionError): - _assert_weak_lock_text_contract( - yaml.safe_dump(lock, sort_keys=False), next_value, sharp_value - ) + _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) From 88467c00f722607acac1cfebe3269bed049b4c73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 14:57:08 +0900 Subject: [PATCH 21/31] chore(review): use central OpenCode configuration --- AGENTS.md | 7 ++- ARCHITECTURE.md | 6 +- CHANGELOG.md | 4 ++ CLAUDE.md | 3 + README.md | 3 +- backend/tests/test_release_governance.py | 8 ++- docs/product-technical-gap-baseline.md | 16 +++++ opencode.jsonc | 77 ------------------------ 8 files changed, 42 insertions(+), 82 deletions(-) delete mode 100644 opencode.jsonc diff --git a/AGENTS.md b/AGENTS.md index 9104dd1f4..1231a4f5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -653,8 +653,11 @@ in this repo. `codegraph init -i` autonomously without asking first; keep generated `.codegraph/` and `.cursor/rules/codegraph.mdc` artifacts local unless a future repository policy explicitly says to commit them. OpenCode PR review - uses the project `opencode.jsonc` MCP servers for CodeGraph, DeepWiki, - Context7, and web search. It must initialize CodeGraph before review so + receives the canonical `opencode.jsonc` from the central required workflow; + do not add repository-local `opencode.json` or `opencode.jsonc` files. The + central runtime owns model routing and MCP registration, including Graphify; + consumer repositories own no provider or MCP fallback configuration. It must + initialize CodeGraph before review so structural findings cite graph-backed evidence instead of relying only on grep or raw file reads; use Context7 for current library docs, DeepWiki for repository documentation, and web search only for bounded external lookups. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9d2cbba18..99580f42c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -232,7 +232,11 @@ and grounding: [`docs/architecture/kg-extractor-seam.md`](docs/architecture/kg-e OpenCode Review, Strix Security Scan, and PR Review Merge Scheduler are supplied by the ContextualWisdomLab central required workflows in `ContextualWisdomLab/.github`; this repository does not carry repo-local copies -of those workflows or their dedicated support scripts. The central Strix +of those workflows, their dedicated support scripts, or OpenCode JSON files. +The required review runtime copies the central `opencode.jsonc` into its +exact-head isolation directory, so a repository-local `opencode.json` or +`opencode.jsonc` would only create an unaudited provider and MCP policy fork. +The central Strix workflow treats pull request code as untrusted whenever repository secrets are available: privileged PR scans run from trusted workflow code, fetch the pull request head as Git objects, and copy PR-head blobs into temporary scan scopes diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec84c36f..4efa7af58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ ## [Unreleased] +- OpenCode 모델·MCP 정책의 저장소별 복사본을 제거했습니다. Naruon의 required + review는 중앙 workflow가 격리 경로에 주입하는 단일 `opencode.jsonc`만 + 사용하며, release-governance 테스트가 두 OpenCode JSON 파일의 재도입을 + 막습니다. - 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다. - EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다. diff --git a/CLAUDE.md b/CLAUDE.md index be67bc80c..c2e9a27ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,9 @@ merged by metadata-only robot governance (see `docs/development/merge-gate-policy.md`); OpenCode Review, Strix Security Scan, and the merge scheduler come from central workflows in `ContextualWisdomLab/.github` — do not reintroduce repo-local copies. +That owner also supplies the only `opencode.jsonc` used by required review; +do not add a repository-local `opencode.json` or `opencode.jsonc`. A local copy +forks model and MCP policy without changing the isolated central review runtime. ## Common commands diff --git a/README.md b/README.md index ff64840e9..5518699db 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ mail/calendar/file systems. - OpenCode Review, Strix Security Scan, and PR Review Merge Scheduler are supplied by the ContextualWisdomLab central required workflows from `ContextualWisdomLab/.github`. This repository does not carry repo-local - OpenCode, Strix, or merge-scheduler workflow copies; branch updates, + OpenCode configuration copies, Strix workflows, or merge-scheduler workflows; + branch updates, auto-merge, and mechanical merge actions run as the target repository's `github-actions[bot]` through the central workflow. Pending CodeRabbit or required-check evidence is a wait state, not a hard blocker. diff --git a/backend/tests/test_release_governance.py b/backend/tests/test_release_governance.py index a23c70746..f33976271 100644 --- a/backend/tests/test_release_governance.py +++ b/backend/tests/test_release_governance.py @@ -633,14 +633,20 @@ def test_review_automation_uses_central_required_workflows_without_local_copies( f"central review automation must not be copied locally: {relative_path}" ) + for relative_path in ("opencode.json", "opencode.jsonc"): + assert not (REPO_ROOT / relative_path).exists(), ( + f"central OpenCode configuration must not be copied locally: {relative_path}" + ) + assert "ContextualWisdomLab central required workflows" in normalized_readme assert "This repository does not carry repo-local" in normalized_readme - assert "OpenCode, Strix, or merge-scheduler workflow copies" in normalized_readme + assert "OpenCode configuration copies" in normalized_readme assert ( "branch updates, auto-merge, and mechanical merge actions" in normalized_readme ) assert "central required workflows" in architecture assert "ContextualWisdomLab/.github" in architecture + assert "central `opencode.jsonc`" in architecture assert "central required workflow" in normalized_security assert "openai/openai/gpt-4.1" not in architecture diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 98bc17d2a..382226e90 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,21 @@ # Naruon Product and Technical Gap Baseline +## 2026-09-09 중앙 OpenCode 설정 소비 경계 + +- **Gap:** Naruon의 저장소별 `opencode.jsonc`가 모델과 MCP를 별도로 선언해 + 중앙 required review의 격리 설정과 서로 다른 정책을 만들 수 있었다. +- **조치:** 저장소별 OpenCode JSON을 제거하고, release-governance 계약이 + `opencode.json`과 `opencode.jsonc`의 재도입을 모두 거부하도록 고정했다. + README·ARCHITECTURE·AGENTS·CLAUDE는 중앙 owner가 exact-head 격리 경로에 + 설정을 주입한다는 같은 책임 경계를 설명한다. +- **선행조건:** 중앙 Graphify owner + [ContextualWisdomLab/.github#2052](https://github.com/ContextualWisdomLab/.github/pull/2052)가 + 보호 병합되기 전까지 이 consumer delta는 Proposed/Draft다. 중앙 실패 시 + consumer 파일을 되살리는 대신 owner를 수리한다. +- **검증 범위:** focused release-governance 테스트와 파일 부재 검증이다. + 실제 중앙 review 실행·Graphify handshake·보호 병합은 owner PR의 exact-head + evidence이며 이 변경만으로 재증명하지 않는다. + **Baseline version:** 1.2 **Observed on:** 2026-08-26 (Asia/Seoul) **Observed protected branch (current scan; row Base-SHA values remain historical):** `develop@e5e99b4e3bb081b92c602358878856536030e2ca` diff --git a/opencode.jsonc b/opencode.jsonc deleted file mode 100644 index 536d212fb..000000000 --- a/opencode.jsonc +++ /dev/null @@ -1,77 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "model": "github-models/openai/gpt-5", - "small_model": "github-models/deepseek/deepseek-v3-0324", - "enabled_providers": ["github-models"], - "mcp": { - "codegraph": { - "type": "local", - "command": ["npx", "-y", "@colbymchenry/codegraph@0.9.9", "serve", "--mcp"], - "enabled": true - }, - "deepwiki": { - "type": "remote", - "url": "https://mcp.deepwiki.com/mcp", - "enabled": true, - "timeout": 300000 - }, - "context7": { - "type": "local", - "command": ["npx", "-y", "@upstash/context7-mcp@3.1.0", "--transport", "stdio"], - "enabled": true, - "timeout": 300000, - "environment": { - "NPM_CONFIG_IGNORE_SCRIPTS": "true", - "NPM_CONFIG_LOGLEVEL": "error" - } - }, - "web_search": { - "type": "local", - "command": ["npx", "-y", "@guhcostan/web-search-mcp@1.0.5"], - "enabled": true, - "timeout": 300000, - "environment": { - "NPM_CONFIG_IGNORE_SCRIPTS": "true", - "NPM_CONFIG_LOGLEVEL": "error" - } - } - }, - "provider": { - "github-models": { - "npm": "@ai-sdk/openai-compatible", - "name": "GitHub Models", - "options": { - "baseURL": "https://models.github.ai/inference", - "apiKey": "{env:STRIX_GITHUB_MODELS_TOKEN}" - }, - "models": { - "openai/gpt-5": { - "name": "OpenAI GPT-5", - "tool_call": true, - "reasoning": true, - "limit": { - "context": 200000, - "output": 100000 - } - }, - "deepseek/deepseek-r1-0528": { - "name": "DeepSeek R1 0528", - "tool_call": true, - "reasoning": true, - "limit": { - "context": 128000, - "output": 4096 - } - }, - "deepseek/deepseek-v3-0324": { - "name": "DeepSeek V3 0324", - "tool_call": true, - "limit": { - "context": 128000, - "output": 4096 - } - } - } - } - } -} From 19d54c334ad75e9657a952350b177180c63adf8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:15:43 +0900 Subject: [PATCH 22/31] test(review): narrow central OpenCode consumer contract --- AGENTS.md | 110 ++++-- ARCHITECTURE.md | 73 ++-- CLAUDE.md | 24 +- README.md | 3 +- .../tests/test_agent_llm_authority_docs.py | 83 ++++ .../test_frontend_framework_security_floor.py | 166 ++++++++ .../test_opencode_central_config_consumer.py | 16 + backend/tests/test_release_governance.py | 22 +- frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 370 ++++++++++-------- frontend/pnpm-workspace.yaml | 2 +- 11 files changed, 612 insertions(+), 261 deletions(-) create mode 100644 backend/tests/test_agent_llm_authority_docs.py create mode 100644 backend/tests/test_frontend_framework_security_floor.py create mode 100644 backend/tests/test_opencode_central_config_consumer.py diff --git a/AGENTS.md b/AGENTS.md index 1231a4f5d..1ccdd17ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,16 @@ # AGENTS.md + +> **Agents: read the master context FIRST.** Before any work, read: +> +> - [`docs/architecture/naruon-product-spec.md`](docs/architecture/naruon-product-spec.md) for product vision, architecture, RBAC/ABAC, core features, AI agent ontology, branding/UX, observability, and governance; +> - the live [GitHub Project #1](https://github.com/orgs/ContextualWisdomLab/projects/1), the work and roadmap source of truth; +> - [`docs/product-technical-gap-baseline.md`](docs/product-technical-gap-baseline.md), the live gap snapshot but not merge authorization, and canonical completion issue [#1428](https://github.com/ContextualWisdomLab/naruon/issues/1428); +> - `ContextualWisdomLab/.github`'s [`docs/agent-github-project-protocol.md`](https://github.com/ContextualWisdomLab/.github/blob/main/docs/agent-github-project-protocol.md) for Project operation; and +> - `ContextualWisdomLab/.github`'s [`docs/product-goal-directive.md`](https://github.com/ContextualWisdomLab/.github/blob/main/docs/product-goal-directive.md) before running or configuring the continuous PR review→fix→merge→develop loop. A length-capped `/goal` pointer does not replace the full directive. +> +> This repository's Figma file ID is not yet canonical on `develop`: ADR-0013 and file ID `68b5XB58w8nwT2LYOOnikK` remain Proposed in open PR [#1436](https://github.com/ContextualWisdomLab/naruon/pull/1436). The repository and Project, not private agent memory, are authoritative. + ## Agent guidance (CWL governance) @@ -136,35 +147,23 @@ in this repo. `.github/workflows/opencode-review.yml`, `.github/workflows/strix.yml`, `.github/workflows/strix-selftest.yml`, or `.github/workflows/pr-review-merge-scheduler.yml`. -- The central Strix Security Scan uses GitHub Models by default through - `STRIX_GITHUB_MODELS_TOKEN`, `STRIX_LLM=openai/gpt-5`, and - `LLM_API_BASE_FILE` pointing at a trusted file containing - `https://models.github.ai/inference`; GitHub Models scans must try the - configured GPT-5-or-newer model first and may fall back to the explicit - workflow fallback list, currently - `github_models/deepseek/deepseek-r1-0528` and - `github_models/deepseek/deepseek-v3-0324`, when GitHub Models provider - capacity or model availability blocks the primary run. The Strix gate must - route these fallback names through the GitHub Models endpoint with - OpenAI-compatible child model names such as - `openai/deepseek/deepseek-r1-0528`, not the public DeepSeek API. Do not use - GPT-4.1 or weaker GitHub Models fallbacks for Strix or OpenCode PR review - evidence. Keep the GitHub Models endpoint in a trusted input file and pass - the token only through - the provider-scoped Strix child-process key path. Legacy `STRIX_LLM` secrets - must not override PR, push, or scheduled Strix defaults. Vertex remains - available only for manual - `workflow_dispatch` evidence when the `strix_llm` input - explicitly selects `vertex_ai/gemini-3.1-pro-preview-customtools` or - `vertex_ai/gemini-2.5-flash` with `GCP_SA_KEY`; expose Google/Vertex - credentials only for Vertex provider mode. Direct OpenAI GPT-5.4-or-newer - scans remain supported only for manual `strix_llm` selections with - `STRIX_OPENAI_API_KEY`. Do not silently fall back between providers, and - do not treat timeout-class provider infrastructure failures as clean PR - evidence even when Strix printed zero vulnerabilities before failing. Disable - silent Vertex fallback models in the workflow unless a future PR proves a new - exact fallback contract with no Timeout/Fatal/Warn/Denied output. Record - provider evidence in the PR. Known third-party Strix/Pydantic +- Central model-backed Actions are owned by `ContextualWisdomLab/.github` and + request only the logical `orchestrator/free` pool with a gateway token. Thin + callers in this repository must not select or forward provider names, model + names, provider groups, endpoints, or paid fallback credentials. +- `ContextualWisdomLab/contextual-orchestrator` owns provider discovery, + capability-based routing, free-pool membership, and fallback. Naruon owns + product/domain truth, authorization, tools, and context assembly. Production + use requires an immutable released owner API/client/schema; verify protected + release evidence before adoption and fail closed when that contract or a + required capability is unavailable. Do not copy owner source, query owner + storage, or treat an open PR or unreleased branch as a consumable contract. +- Do not impose a shared application/agent/gateway wall-clock timeout on model + work. A configured administrator limit, explicit user cancellation, or the + upstream provider's terminal result may end it; reasoning, streaming, and + tool calls are not failed merely because elapsed time is long. +- A failed model-backed check remains failed evidence even when an intermediate + report printed zero findings. Known third-party Strix/Pydantic serializer warnings must be filtered narrowly inside the Strix gate child process, not as a visible workflow env entry, so Warn-class logs are not accepted as clean evidence and warning-filter variable names do not pollute @@ -274,6 +273,19 @@ in this repo. ## Workspace and task tracking defaults +- Browser smoke must check collected console and page errors after interaction, + accessibility, screenshot capture, and page cleanup, not only after navigation. + Keep the early check for fast failure and a final check before returning success. + Regression tests must inject late errors into the actual route-smoke execution; + a successful screenshot or a passing render assertion does not prove clean interactions. + If screenshot retries are exhausted, throw the capture failure; do not create + diagnostic text files in the temporary capture directory or count them as screenshots. + Close the page in a finally block, including navigation and capture failures. +- Browser-smoke API mocks must reject unregistered requests with a non-success + response and record a harness failure even when the UI suppresses the error. + Cover requests emitted during cleanup; never return a default `{ ok: true }`. + Keep request URLs, query values, and payloads out of failure diagnostics. + - First-run frontend sessions should open the Today execution dashboard while preserving explicit Dashboard, Email, and Calendar startup choices. - Workspace navigation changes must keep the desktop primary nav and the @@ -457,10 +469,10 @@ in this repo. update both backend header tests and frontend link tests. Global backend responses must include `Referrer-Policy`, and `target="_blank"` links must use explicit `rel="noopener noreferrer"`. -- When robot review cites an obsolete Strix provider policy, update the docs and - tests to the current GitHub Models default contract before accepting a - rollback suggestion; do not reintroduce generic `LLM_API_KEY` or - cross-provider credential forwarding while trying to satisfy old comments. +- When robot review cites obsolete provider-specific Strix policy, compare it + with the released `.github` and `contextual-orchestrator` contracts before + accepting a rollback suggestion. Do not reintroduce direct-provider routing, + generic provider credentials, or cross-provider credential forwarding. - When reviews find inert navigation/dead-space controls, either wire them to an implemented workspace route/API or remove the control; do not leave high-traffic drawer/sidebar entries as permanent `준비 중` copy. @@ -649,15 +661,37 @@ in this repo. ## Development environment and tooling defaults +- Read the applicable repository skill before changing its contract: + [fix-development-mistakes](.agents/skills/fix-development-mistakes/SKILL.md) + for failures and security findings, + [github-actions-privileged-pr-scan](.agents/skills/github-actions-privileged-pr-scan/SKILL.md) + for privileged PR scanners, and + [github-robot-review-gate](.agents/skills/github-robot-review-gate/SKILL.md) + for check/review diagnosis. Record the failing reproduction, smallest causal + repair, exact verification command, and remaining gates in the existing PR. +- Visual Inspection requires opening the actual rendered pages or captured + images, not merely counting PNG files. Record app/build SHA separately from + runner SHA, viewport, routes, and observed defects. Mocked browser evidence + does not prove live provider behavior, all locales, or deployment readiness. + Inspect the changed AGENTS.md rendering on the pushed revision as well. +- A server-ready timeout before browser navigation is failed startup evidence, + not a completed visual inspection. Record the exact source and execution result; + host load alone does not prove the cause or justify increasing test timeouts. +- Preserve concurrent commits with ordinary history integration, then verify + the combined tree before pushing. Update these instructions with reusable + failure-prevention lessons; keep dated findings in the gap baseline/PR. +- Package registry credentials alone do not authorize or configure a cluster + deployment. Use the existing release workflow only after its protected-source, + checks, target credentials, and destination prerequisites are verified; report + package publication and live deployment as separate outcomes. +- Before relocating guidance out of a PR, verify its complete successor delta + and record the destination PR, commit, and path; a named owner alone is not succession. - If CodeGraph is not initialized for this repository, agents may run `codegraph init -i` autonomously without asking first; keep generated `.codegraph/` and `.cursor/rules/codegraph.mdc` artifacts local unless a future repository policy explicitly says to commit them. OpenCode PR review - receives the canonical `opencode.jsonc` from the central required workflow; - do not add repository-local `opencode.json` or `opencode.jsonc` files. The - central runtime owns model routing and MCP registration, including Graphify; - consumer repositories own no provider or MCP fallback configuration. It must - initialize CodeGraph before review so + uses the project `opencode.jsonc` MCP servers for CodeGraph, DeepWiki, + Context7, and web search. It must initialize CodeGraph before review so structural findings cite graph-backed evidence instead of relying only on grep or raw file reads; use Context7 for current library docs, DeepWiki for repository documentation, and web search only for bounded external lookups. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 99580f42c..0ad757b07 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -6,7 +6,7 @@ flowchart LR UI[Next.js frontend] --> API[FastAPI backend / Naruon control plane] API --> DB[(Postgres + pgvector)] - API --> LLM[OpenAI APIs when configured] + API --> LLM[contextual-orchestrator released consumer API] API --> CONN[Outbound-only self-hosted connector] CONN --> MAIL[Customer IMAP/POP3/SMTP] CONN --> DAV[Customer CalDAV/CardDAV/WebDAV] @@ -20,6 +20,15 @@ Runtime database connectivity is secret-injected: `backend/core/config.py` has no fallback `DATABASE_URL`, so missing database configuration fails at startup rather than silently using shared development credentials. +Production LLM work crosses an immutable released `contextual-orchestrator` +API/client/schema. `contextual-orchestrator` owns provider discovery, +capability-based routing, free-pool membership, and provider fallback; Naruon +owns product/domain truth, authorization, tools, and context assembly. A missing +or incompatible released owner contract fails closed instead of creating a +Naruon-local provider/model fallback. Local deterministic fixtures and legacy +provider surfaces may remain for migration/testing only when they are explicitly +identified as non-production authority. + ## Topic-intelligence boundary Naruon has no live Structural Topic Modeling endpoint, fitted topic artifact, @@ -83,7 +92,10 @@ registry/list/update/delete plus prompt-preview provider selection paths filter the authenticated organization. Existing local databases get the same fail-closed owner backfill through `scripts/bootstrap_db.py`; legacy provider rows require explicit non-default `NARUON_IMPORT_USER_ID` and `NARUON_IMPORT_ORGANIZATION_ID` -before bootstrap will set the new columns non-null. +before bootstrap will set the new columns non-null. This table and its direct +provider-routing fields are legacy migration surfaces, not the production +provider/model authority; new production LLM work must consume the released +`contextual-orchestrator` contract instead of extending this registry as a router. `ticket_tasks` stores email-derived execution items as ticket-like work records. The table and its new columns use at least two-word `snake_case` database names @@ -170,13 +182,15 @@ ETag/If-Match-guarded CalDAV/WebDAV PUT local adapter handlers for `SelfHostedConnector`; packaging, registration-token lifecycle, backend dispatch, and non-DAV protocol adapters remain separate connector delivery work. -LLM provider `base_url` is also a server-side egress boundary, not an arbitrary -URL field. Provider registry create/update paths are organization-admin scoped, -and LLM call sinks validate custom OpenAI-compatible base URLs with HTTPS-only -syntax, no userinfo/query or fragment, exact host membership in -`ALLOWED_LLM_BASE_URL_HOSTS`, and DNS answers that are all globally routable. -Missing allowlist configuration fails closed; the default provider path should -leave `base_url` unset. +The legacy LLM-provider `base_url` path remains a server-side egress boundary +while migration is incomplete, not an arbitrary URL field or production routing +authority. Existing provider registry create/update paths remain +organization-admin scoped, and current call sinks retain HTTPS-only syntax, +userinfo/query/fragment rejection, exact `ALLOWED_LLM_BASE_URL_HOSTS` membership, +and globally routable DNS validation until the legacy path is removed. Missing +allowlist configuration still fails closed. New production model work must not +select a provider endpoint through this surface; it uses the immutable released +`contextual-orchestrator` consumer contract. ## Batch embedding routing boundary @@ -192,12 +206,13 @@ SSRF-guarded, allowlisted, pinned-address HTTP client (`build_llm_provider_http_client`) that fronts every other outbound LLM call. naruon records a durable `llm_batch_jobs` / `llm_batch_items` audit trail (routing mode, the orchestrator's batch id, reported cost) for observability. -The path degrades gracefully: if batching is disabled, the orchestrator base URL -is rejected by the egress guard, or the orchestrator is unreachable, the caller -transparently falls back to the per-item embedding path. A local `pg-llm-batch` -package/checkout remains only as an optional offline-dev fallback, gated behind -orchestrator-unavailable and an explicit local DSN; naruon does not vendor a -gitlink for that fallback in this PR. +The currently implemented per-item fallback is migration behavior rather than +canonical provider authority: if batching is disabled or the orchestrator path +cannot be used, callers may still reach the legacy per-item embedding path, but +that path must not grow new production provider/model selection. A local +`pg-llm-batch` package/checkout remains only as an optional offline-dev fallback, +gated behind orchestrator-unavailable and an explicit local DSN; naruon does not +vendor a gitlink for that fallback in this PR. ## Semantic project-graph extractor seam @@ -216,27 +231,23 @@ and the runner degrades down the chain instead of losing the projection. New extractors (including future plugins on the platform plan's `kg.extractor` extension point) register a selector without editing ingest. -Routing LLM extraction through **contextual-orchestrator** is modelled as a -transport concern: the orchestrator is an OpenAI-compatible gateway, so the -`orchestrator` selector reuses the identical grounded LLM extractor -(`extract_project_semantics_llm`, which enforces segment citations) but points -its SSRF-allowlisted client (`build_llm_provider_http_client`) at -`PROJECT_GRAPH_ORCHESTRATOR_BASE_URL` instead of the raw provider. The provider -API key stays the tenant's Fernet-encrypted credential, and the orchestrator base -URL must be HTTPS and exact-host allowlisted by `ALLOWED_LLM_BASE_URL_HOSTS`; -an unset or rejected endpoint fails closed to the deterministic extractor. Design -and grounding: [`docs/architecture/kg-extractor-seam.md`](docs/architecture/kg-extractor-seam.md). +The current `orchestrator` selector still reuses the grounded LLM extractor and +an OpenAI-compatible transport adapter, but that compatibility layer is legacy +migration plumbing rather than Naruon-owned provider/model routing. It points the +SSRF-allowlisted client (`build_llm_provider_http_client`) at +`PROJECT_GRAPH_ORCHESTRATOR_BASE_URL`; its existing credential and allowlist +handling remains in place until the released owner client replaces it. New +production behavior must consume the immutable released `contextual-orchestrator` +API/client/schema and must not add raw-provider fallback here. An unset or +rejected endpoint fails closed to the deterministic extractor. Design and +grounding: [`docs/architecture/kg-extractor-seam.md`](docs/architecture/kg-extractor-seam.md). ## CI security boundary OpenCode Review, Strix Security Scan, and PR Review Merge Scheduler are supplied by the ContextualWisdomLab central required workflows in `ContextualWisdomLab/.github`; this repository does not carry repo-local copies -of those workflows, their dedicated support scripts, or OpenCode JSON files. -The required review runtime copies the central `opencode.jsonc` into its -exact-head isolation directory, so a repository-local `opencode.json` or -`opencode.jsonc` would only create an unaudited provider and MCP policy fork. -The central Strix +of those workflows or their dedicated support scripts. The central Strix workflow treats pull request code as untrusted whenever repository secrets are available: privileged PR scans run from trusted workflow code, fetch the pull request head as Git objects, and copy PR-head blobs into temporary scan scopes @@ -323,4 +334,4 @@ target source id, but it must not provide source ownership or capability records `backend/api/calendar.py` obtains writeback sources through a FastAPI dependency that reads DB-backed `calendar_writeback_sources` records scoped to the authenticated user. Provider execution remains opt-in and fail-closed without an -active outbound runner or If-Match/ETag evidence. +active outbound runner or If-Match/ETag evidence. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index c2e9a27ef..0bfd4f580 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,6 @@ merged by metadata-only robot governance (see `docs/development/merge-gate-policy.md`); OpenCode Review, Strix Security Scan, and the merge scheduler come from central workflows in `ContextualWisdomLab/.github` — do not reintroduce repo-local copies. -That owner also supplies the only `opencode.jsonc` used by required review; -do not add a repository-local `opencode.json` or `opencode.jsonc`. A local copy -forks model and MCP policy without changing the isolated central review runtime. ## Common commands @@ -113,11 +110,22 @@ indexes, and auditable writeback intent. `ARCHITECTURE.md` and ``` Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgvector │ - ├──> OpenAI-compatible LLM providers (Ollama locally) + ├──> contextual-orchestrator consumer boundary + │ └──> provider/model routing owned by contextual-orchestrator └──> outbound-only self-hosted connector (connector/) └──> customer IMAP/POP3/SMTP + CalDAV/CardDAV/WebDAV ``` +Naruon owns product behavior and model-call authorization, but not provider/model routing. +Production model work requires an immutable released `contextual-orchestrator` +API/client/schema; a missing or incompatible owner contract fails closed instead +of falling back to a direct provider. The Ollama/MLX paths in local Compose are +explicit non-production development fixtures. Central model-backed GitHub Actions +are owned by `ContextualWisdomLab/.github` and use `orchestrator/free`; do not copy +that pool selection into Naruon runtime. Verify protected release evidence before +claiming the owner contract is available; an open PR or unreleased branch is only +proposed evidence. + - `backend/` — FastAPI app (`main.py`, routers in `api/`, domain logic in `services/`, SQLAlchemy models in `db/`, Alembic in `alembic/`). Owns persistence, canonical email threading @@ -143,8 +151,12 @@ Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgve ETag/If-Match); intents are the default response. - Auth: signed HS256 bearer sessions (HMAC via `AUTH_SESSION_HMAC_SECRET`) or enterprise OIDC/JWKS; private `/api/*` routers register the default - `get_auth_context` dependency. LLM `base_url` and OIDC/SMTP/IMAP/POP3 hosts - are strict egress allowlists that resolve only to pinned global addresses. + `get_auth_context` dependency. Naruon owns domain tools, authorization, and + context assembly; provider/model routing belongs to `contextual-orchestrator`. + Existing direct-provider `base_url` surfaces are legacy migration debt and must + not be extended as production routing authority; keep their SSRF controls until + they are removed. OIDC/SMTP/IMAP/POP3 hosts remain strict egress allowlists + that resolve only to pinned global addresses. - CI (`.github/workflows/`): `app-ci.yml` (backend ruff+pytest, frontend test/lint/build), plus `bandit`, `codeql`, `trivy`, `scorecard`, `pr-governance`, `docker-publish` (GHCR on `v*` tags matching `VERSION`), and diff --git a/README.md b/README.md index 5518699db..ff64840e9 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,7 @@ mail/calendar/file systems. - OpenCode Review, Strix Security Scan, and PR Review Merge Scheduler are supplied by the ContextualWisdomLab central required workflows from `ContextualWisdomLab/.github`. This repository does not carry repo-local - OpenCode configuration copies, Strix workflows, or merge-scheduler workflows; - branch updates, + OpenCode, Strix, or merge-scheduler workflow copies; branch updates, auto-merge, and mechanical merge actions run as the target repository's `github-actions[bot]` through the central workflow. Pending CodeRabbit or required-check evidence is a wait state, not a hard blocker. diff --git a/backend/tests/test_agent_llm_authority_docs.py b/backend/tests/test_agent_llm_authority_docs.py new file mode 100644 index 000000000..e8dcc9c40 --- /dev/null +++ b/backend/tests/test_agent_llm_authority_docs.py @@ -0,0 +1,83 @@ +"""Regression contracts for current LLM-routing authority guidance.""" + +import json +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _read(path: str) -> str: + """Read repository guidance as UTF-8 text.""" + return (REPO_ROOT / path).read_text(encoding="utf-8") + + +def test_agent_guidance_does_not_reintroduce_direct_model_routing_authority() -> None: + """Current agent and architecture guidance must not prescribe direct routing.""" + agents = _read("AGENTS.md") + claude = _read("CLAUDE.md") + architecture = _read("ARCHITECTURE.md") + + forbidden_current_guidance = ( + "STRIX_GITHUB_MODELS_TOKEN", + "https://models.github.ai/inference", + "Direct OpenAI GPT-5.4-or-newer", + "OpenAI-compatible LLM providers", + "API --> LLM[OpenAI APIs when configured]", + ) + combined = f"{agents}\n{claude}\n{architecture}" + for phrase in forbidden_current_guidance: + assert phrase not in combined + + +def test_agent_guidance_names_canonical_llm_owner_and_fail_closed_boundary() -> None: + """Guidance must preserve product ownership while delegating LLM routing.""" + agents = _read("AGENTS.md") + claude = _read("CLAUDE.md") + architecture = _read("ARCHITECTURE.md") + + assert "ContextualWisdomLab/.github" in agents + assert "contextual-orchestrator" in agents + assert "orchestrator/free" in agents + assert "fail closed" in agents.lower() + assert "contextual-orchestrator" in claude + assert "Naruon owns" in claude + assert "provider/model routing" in claude + assert "gateway token" in agents + assert "provider names, model" in agents + assert "immutable released owner API/client/schema" in agents + assert "open PR or unreleased branch" in agents + assert "shared application/agent/gateway wall-clock timeout" in agents + assert "contextual-orchestrator" in architecture + assert "provider discovery" in architecture + assert "immutable released" in architecture + assert "fails closed" in architecture + + +def test_opencode_config_uses_only_contextual_orchestrator_free() -> None: + """Repository OpenCode model work must use only the canonical logical pool.""" + raw_config = _read("opencode.jsonc") + config = json.loads(raw_config) + + assert config["model"] == "contextual-orchestrator/orchestrator/free" + assert config["small_model"] == "contextual-orchestrator/orchestrator/free" + assert config["enabled_providers"] == ["contextual-orchestrator"] + assert set(config["provider"]) == {"contextual-orchestrator"} + + provider = config["provider"]["contextual-orchestrator"] + assert provider["options"] == { + "baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}", + "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}", + } + assert set(provider["models"]) == {"orchestrator/free"} + + forbidden_direct_routing = ( + "github-models", + "STRIX_GITHUB_MODELS_TOKEN", + "https://models.github.ai/inference", + '"openai/gpt-5"', + '"deepseek/deepseek-r1-0528"', + '"deepseek/deepseek-v3-0324"', + ) + for phrase in forbidden_direct_routing: + assert phrase not in raw_config diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py new file mode 100644 index 000000000..bf318b5ad --- /dev/null +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -0,0 +1,166 @@ +"""Fail closed when frontend framework/image dependencies regress below patched floors.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[2] +FRONTEND_ROOT = REPO_ROOT / "frontend" +NEXT_SECURITY_FLOOR = (16, 3, 3) +SHARP_SECURITY_FLOOR = (0, 35, 4) + + +def _exact_version(value: str) -> tuple[int, int, int]: + """Return a three-part exact version, rejecting ranges and prereleases.""" + + match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", value) + assert match is not None, f"expected exact semantic version, got {value!r}" + return tuple(int(part) for part in match.groups()) + + +def _resolved_version(value: str) -> tuple[int, int, int]: + """Return the exact version prefix from a pnpm peer-qualified resolution.""" + + version = value.split("(", 1)[0] + return _exact_version(version) + + +def _package_key_version(package_key: str, package_name: str) -> tuple[int, int, int]: + """Return the version encoded by one pnpm package/snapshot key.""" + + prefix = f"{package_name}@" + assert package_key.startswith(prefix), ( + f"expected {package_name!r} lock key, got {package_key!r}" + ) + return _resolved_version(package_key[len(prefix) :]) + + +def _assert_lock_contract( + lock: dict[str, Any], + next_value: str, + eslint_next_value: str, + sharp_value: str, +) -> None: + """Validate root resolution identity and every locked Next.js/sharp security floor.""" + + importer = lock["importers"]["."] + next_import = importer["dependencies"]["next"] + assert next_import["specifier"] == next_value, ( + "root importer must preserve the package.json Next.js specifier" + ) + assert _resolved_version(str(next_import["version"])) == _exact_version(next_value), ( + "root importer must resolve the reviewed Next.js release" + ) + assert f"next@{next_import['version']}" in lock["snapshots"], ( + "root importer Next.js resolution must reference an existing snapshot" + ) + + eslint_next_import = importer["devDependencies"]["eslint-config-next"] + assert eslint_next_import["specifier"] == eslint_next_value, ( + "root importer must preserve the eslint-config-next specifier" + ) + assert _resolved_version(str(eslint_next_import["version"])) == _exact_version( + eslint_next_value + ), "root importer must resolve the reviewed eslint-config-next release" + assert f"eslint-config-next@{eslint_next_import['version']}" in lock["snapshots"], ( + "root importer eslint-config-next resolution must reference an existing snapshot" + ) + + assert str(lock["overrides"]["sharp"]) == sharp_value, ( + "lockfile sharp override must match the reviewed workspace override" + ) + + expected_next = _exact_version(next_value) + expected_sharp = _exact_version(sharp_value) + for section_name in ("packages", "snapshots"): + section = lock[section_name] + next_keys = [key for key in section if key.startswith("next@")] + sharp_keys = [key for key in section if key.startswith("sharp@")] + + assert next_keys, f"{section_name} must contain a Next.js resolution" + assert sharp_keys, f"{section_name} must contain a sharp resolution" + assert any( + _package_key_version(key, "next") == expected_next for key in next_keys + ), f"{section_name} must contain the reviewed Next.js release" + assert any( + _package_key_version(key, "sharp") == expected_sharp for key in sharp_keys + ), f"{section_name} must contain the reviewed sharp release" + + for package_key in next_keys: + assert _package_key_version(package_key, "next") >= NEXT_SECURITY_FLOOR, ( + f"{section_name} contains Next.js below the reviewed security floor: " + f"{package_key}" + ) + for package_key in sharp_keys: + assert _package_key_version(package_key, "sharp") >= SHARP_SECURITY_FLOOR, ( + f"{section_name} contains sharp below the reviewed security floor: " + f"{package_key}" + ) + + +def _frontend_security_inputs() -> tuple[str, str, str, dict[str, Any]]: + """Load the manifest, workspace override, and generated lock contract.""" + + package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) + next_value = package["dependencies"]["next"] + eslint_next_value = package["devDependencies"]["eslint-config-next"] + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + ) + sharp_value = str(workspace["overrides"]["sharp"]) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + return next_value, eslint_next_value, sharp_value, lock + + +def test_frontend_framework_and_image_security_floors() -> None: + """Keep manifests and every generated lock resolution at reviewed patched releases.""" + + next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() + + assert _exact_version(next_value) >= NEXT_SECURITY_FLOOR, ( + "Next.js must include the fixes for CVE-2026-75604 and " + "GHSA-2xp9-vwfh-vxw4" + ) + assert eslint_next_value == next_value, ( + "eslint-config-next must stay on the same reviewed release as Next.js" + ) + assert _exact_version(sharp_value) >= SHARP_SECURITY_FLOOR, ( + "sharp must include the fix for GHSA-rgj7-g3m4-5g8c" + ) + _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) + + +@pytest.mark.parametrize("field", ["specifier", "version"]) +def test_security_floor_rejects_root_importer_drift(field: str) -> None: + """Reject a partially regenerated lock whose root Next.js importer drifts.""" + + next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() + lock["importers"]["."]["dependencies"]["next"][field] = "16.3.2" + + with pytest.raises(AssertionError): + _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) + + +@pytest.mark.parametrize( + ("section_name", "package_key"), + [("packages", "next@16.3.2"), ("snapshots", "sharp@0.35.3")], +) +def test_security_floor_rejects_every_below_floor_lock_entry( + section_name: str, package_key: str +) -> None: + """Reject any stale vulnerable Next.js or sharp package/snapshot entry.""" + + next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs() + lock[section_name][package_key] = {} + + with pytest.raises(AssertionError): + _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) diff --git a/backend/tests/test_opencode_central_config_consumer.py b/backend/tests/test_opencode_central_config_consumer.py new file mode 100644 index 000000000..2e34e8771 --- /dev/null +++ b/backend/tests/test_opencode_central_config_consumer.py @@ -0,0 +1,16 @@ +"""Contract for consuming OpenCode review configuration from the central owner.""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_required_review_has_no_repository_local_opencode_configuration() -> None: + """Repository-local OpenCode JSON must not fork the central review policy.""" + + for relative_path in ("opencode.json", "opencode.jsonc"): + assert not (REPO_ROOT / relative_path).exists(), ( + "central OpenCode configuration must remain owner-controlled: " + f"{relative_path}" + ) diff --git a/backend/tests/test_release_governance.py b/backend/tests/test_release_governance.py index f33976271..4d879f731 100644 --- a/backend/tests/test_release_governance.py +++ b/backend/tests/test_release_governance.py @@ -601,6 +601,20 @@ def test_scorecard_sarif_normalizer_rejects_escape_links_and_large_input( assert module.main([str(normalizer), str(expected)]) == 65 +def test_agents_entry_points_to_current_product_and_governance_sources() -> None: + agents = read_repo_text("AGENTS.md") + + assert "" in agents + assert "docs/architecture/naruon-product-spec.md" in agents + assert "docs/product-technical-gap-baseline.md" in agents + assert "ContextualWisdomLab/projects/1" in agents + assert "ContextualWisdomLab/naruon/issues/1428" in agents + assert "ContextualWisdomLab/naruon/pull/1436" in agents + assert "docs/agent-github-project-protocol.md" in agents + assert "docs/product-goal-directive.md" in agents + assert "not private agent memory" in agents + + def test_review_automation_uses_central_required_workflows_without_local_copies() -> ( None ): @@ -633,20 +647,14 @@ def test_review_automation_uses_central_required_workflows_without_local_copies( f"central review automation must not be copied locally: {relative_path}" ) - for relative_path in ("opencode.json", "opencode.jsonc"): - assert not (REPO_ROOT / relative_path).exists(), ( - f"central OpenCode configuration must not be copied locally: {relative_path}" - ) - assert "ContextualWisdomLab central required workflows" in normalized_readme assert "This repository does not carry repo-local" in normalized_readme - assert "OpenCode configuration copies" in normalized_readme + assert "OpenCode, Strix, or merge-scheduler workflow copies" in normalized_readme assert ( "branch updates, auto-merge, and mechanical merge actions" in normalized_readme ) assert "central required workflows" in architecture assert "ContextualWisdomLab/.github" in architecture - assert "central `opencode.jsonc`" in architecture assert "central required workflow" in normalized_security assert "openai/openai/gpt-4.1" not in architecture diff --git a/frontend/package.json b/frontend/package.json index 191b7c90b..c86342aa1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.27.0", - "next": "16.2.12", + "next": "16.3.4", "react": "19.2.8", "react-dom": "19.2.8", "react-resizable-panels": "^4.12.2", @@ -39,7 +39,7 @@ "@types/react-dom": "^19", "@vitest/coverage-v8": "4.1.10", "eslint": "^9", - "eslint-config-next": "16.2.12", + "eslint-config-next": "16.3.4", "fast-check": "^4.9.0", "jsdom": "^30.0.1", "postcss": "8.5.24", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 610a0e7ca..add535314 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: overrides: brace-expansion: 5.0.9 postcss: 8.5.24 - sharp: 0.35.0 + sharp: 0.35.4 undici: 8.9.0 pnpmfileChecksum: sha256-RXPq3MmEdRb3xD3rhbER9kciz9nBr/i0J/uMUjql5t0= @@ -38,8 +38,8 @@ importers: specifier: ^1.27.0 version: 1.27.0(react@19.2.8) next: - specifier: 16.2.12 - version: 16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 16.3.4 + version: 16.3.4(@babel/core@7.29.7)(@playwright/test@1.62.0)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: specifier: 19.2.8 version: 19.2.8 @@ -84,8 +84,8 @@ importers: specifier: ^9 version: 9.39.5(jiti@2.7.0) eslint-config-next: - specifier: 16.2.12 - version: 16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + specifier: 16.3.4 + version: 16.3.4(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) fast-check: specifier: ^4.9.0 version: 4.9.0 @@ -289,6 +289,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -369,160 +375,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.0': - resolution: {integrity: sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.0': - resolution: {integrity: sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.0': - resolution: {integrity: sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.0': - resolution: {integrity: sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.0': - resolution: {integrity: sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.0': - resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.0': - resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.0': - resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.0': - resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.0': - resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.0': - resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': - resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.0': - resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.0': - resolution: {integrity: sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.0': - resolution: {integrity: sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.0': - resolution: {integrity: sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.0': - resolution: {integrity: sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.0': - resolution: {integrity: sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.0': - resolution: {integrity: sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.0': - resolution: {integrity: sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.0': - resolution: {integrity: sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.0': - resolution: {integrity: sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.0': - resolution: {integrity: sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.0': - resolution: {integrity: sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.0': - resolution: {integrity: sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.0': - resolution: {integrity: sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -549,60 +555,60 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.12': - resolution: {integrity: sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==} + '@next/env@16.3.4': + resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} - '@next/eslint-plugin-next@16.2.12': - resolution: {integrity: sha512-uF2z/qAK2q7B5/6CpnFcBRX6jOq5iCO+Uqh1UkJhXljX1JwLarLYhhoJadO6dPb6moTprOKewMXheBcbIoSbug==} + '@next/eslint-plugin-next@16.3.4': + resolution: {integrity: sha512-szW9y2Aumu4z88YXfTzcFsgUAg2k64uzbtcO5L9f1AKS4w/GUKJcbFllRflROVyNPgJtGOnvNxiyp3v6b+prIA==} - '@next/swc-darwin-arm64@16.2.12': - resolution: {integrity: sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==} + '@next/swc-darwin-arm64@16.3.4': + resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.12': - resolution: {integrity: sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==} + '@next/swc-darwin-x64@16.3.4': + resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.12': - resolution: {integrity: sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==} + '@next/swc-linux-arm64-gnu@16.3.4': + resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.12': - resolution: {integrity: sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==} + '@next/swc-linux-arm64-musl@16.3.4': + resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.12': - resolution: {integrity: sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==} + '@next/swc-linux-x64-gnu@16.3.4': + resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.12': - resolution: {integrity: sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==} + '@next/swc-linux-x64-musl@16.3.4': + resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.12': - resolution: {integrity: sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==} + '@next/swc-win32-arm64-msvc@16.3.4': + resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.12': - resolution: {integrity: sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==} + '@next/swc-win32-x64-msvc@16.3.4': + resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -893,8 +899,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -1529,8 +1535,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@16.2.12: - resolution: {integrity: sha512-iaaf4vvKo5h2LBdGt0JuRv7t0Ysqr9FMCiFxbptDg8LqOE//mIKR80DdpOnSVM7qjLH3jT8P0aFiwXxBEGZRXw==} + eslint-config-next@16.3.4: + resolution: {integrity: sha512-35/8RM10huEL9vlr8hUZMERMENHBrnyHN3ZZkF9efSgzGaqK34jIqry44A956//zriUhUAUW0XSkcolhrryqAA==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -1622,6 +1628,7 @@ packages: eslint@9.39.5: resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -2259,8 +2266,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@16.2.12: - resolution: {integrity: sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==} + next@16.3.4: + resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -2504,9 +2511,14 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - sharp@0.35.0: - resolution: {integrity: sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -3130,6 +3142,11 @@ snapshots: eslint: 9.39.5(jiti@2.7.0) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@2.7.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.2': {} '@eslint/config-array@0.21.2': @@ -3209,108 +3226,108 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.35.0': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.0 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.35.0': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.0 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.0': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.0 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.0': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.0': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.0': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.0': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.0': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.0': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.0': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.0': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.0': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.35.0': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.0 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.35.0': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.0 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.35.0': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.0 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.35.0': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.0 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.35.0': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.0 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.35.0': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.0 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.0': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.0': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.35.0': + '@img/sharp-wasm32@0.35.4': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.0': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.0 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.35.0': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.35.0': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.35.0': + '@img/sharp-win32-x64@0.35.4': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -3346,34 +3363,37 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.2.12': {} + '@next/env@16.3.4': {} - '@next/eslint-plugin-next@16.2.12': + '@next/eslint-plugin-next@16.3.4(eslint@9.39.5(jiti@2.7.0))': dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)) fast-glob: 3.3.1 + transitivePeerDependencies: + - eslint - '@next/swc-darwin-arm64@16.2.12': + '@next/swc-darwin-arm64@16.3.4': optional: true - '@next/swc-darwin-x64@16.2.12': + '@next/swc-darwin-x64@16.3.4': optional: true - '@next/swc-linux-arm64-gnu@16.2.12': + '@next/swc-linux-arm64-gnu@16.3.4': optional: true - '@next/swc-linux-arm64-musl@16.2.12': + '@next/swc-linux-arm64-musl@16.3.4': optional: true - '@next/swc-linux-x64-gnu@16.2.12': + '@next/swc-linux-x64-gnu@16.3.4': optional: true - '@next/swc-linux-x64-musl@16.2.12': + '@next/swc-linux-x64-musl@16.3.4': optional: true - '@next/swc-win32-arm64-msvc@16.2.12': + '@next/swc-win32-arm64-msvc@16.3.4': optional: true - '@next/swc-win32-x64-msvc@16.2.12': + '@next/swc-win32-x64-msvc@16.3.4': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3584,7 +3604,7 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.15': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -4287,9 +4307,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3): + eslint-config-next@16.3.4(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@next/eslint-plugin-next': 16.2.12 + '@next/eslint-plugin-next': 16.3.4(eslint@9.39.5(jiti@2.7.0)) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) @@ -5055,10 +5075,10 @@ snapshots: natural-compare@1.4.0: {} - next@16.2.12(@babel/core@7.29.7)(@playwright/test@1.62.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.4(@babel/core@7.29.7)(@playwright/test@1.62.0)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.2.12 - '@swc/helpers': 0.5.15 + '@next/env': 16.3.4 + '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.11.5 caniuse-lite: 1.0.30001806 postcss: 8.5.24 @@ -5066,18 +5086,19 @@ snapshots: react-dom: 19.2.8(react@19.2.8) styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.8) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.12 - '@next/swc-darwin-x64': 16.2.12 - '@next/swc-linux-arm64-gnu': 16.2.12 - '@next/swc-linux-arm64-musl': 16.2.12 - '@next/swc-linux-x64-gnu': 16.2.12 - '@next/swc-linux-x64-musl': 16.2.12 - '@next/swc-win32-arm64-msvc': 16.2.12 - '@next/swc-win32-x64-msvc': 16.2.12 + '@next/swc-darwin-arm64': 16.3.4 + '@next/swc-darwin-x64': 16.3.4 + '@next/swc-linux-arm64-gnu': 16.3.4 + '@next/swc-linux-arm64-musl': 16.3.4 + '@next/swc-linux-x64-gnu': 16.3.4 + '@next/swc-linux-x64-musl': 16.3.4 + '@next/swc-win32-arm64-msvc': 16.3.4 + '@next/swc-win32-x64-msvc': 16.3.4 '@playwright/test': 1.62.0 - sharp: 0.35.0 + sharp: 0.35.4(@types/node@26.1.2) transitivePeerDependencies: - '@babel/core' + - '@types/node' - babel-plugin-macros node-exports-info@1.6.2: @@ -5338,37 +5359,38 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 - sharp@0.35.0: + sharp@0.35.4(@types/node@26.1.2): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.0 - '@img/sharp-darwin-x64': 0.35.0 - '@img/sharp-freebsd-wasm32': 0.35.0 - '@img/sharp-libvips-darwin-arm64': 1.3.0 - '@img/sharp-libvips-darwin-x64': 1.3.0 - '@img/sharp-libvips-linux-arm': 1.3.0 - '@img/sharp-libvips-linux-arm64': 1.3.0 - '@img/sharp-libvips-linux-ppc64': 1.3.0 - '@img/sharp-libvips-linux-riscv64': 1.3.0 - '@img/sharp-libvips-linux-s390x': 1.3.0 - '@img/sharp-libvips-linux-x64': 1.3.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 - '@img/sharp-linux-arm': 0.35.0 - '@img/sharp-linux-arm64': 0.35.0 - '@img/sharp-linux-ppc64': 0.35.0 - '@img/sharp-linux-riscv64': 0.35.0 - '@img/sharp-linux-s390x': 0.35.0 - '@img/sharp-linux-x64': 0.35.0 - '@img/sharp-linuxmusl-arm64': 0.35.0 - '@img/sharp-linuxmusl-x64': 0.35.0 - '@img/sharp-webcontainers-wasm32': 0.35.0 - '@img/sharp-win32-arm64': 0.35.0 - '@img/sharp-win32-ia32': 0.35.0 - '@img/sharp-win32-x64': 0.35.0 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 26.1.2 optional: true shebang-command@2.0.0: diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index d028031d2..784b361c1 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -16,7 +16,7 @@ supportedArchitectures: overrides: brace-expansion: "5.0.9" postcss: "8.5.24" - sharp: "0.35.0" + sharp: "0.35.4" undici: 8.9.0 patchedDependencies: From 09500e4ed7be4a3d770595c1cbf3bddf1c7bdb2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:19:43 +0900 Subject: [PATCH 23/31] fix(review): keep OpenCode config owner-controlled --- AGENTS.md | 6 ++-- CHANGELOG.md | 2 ++ CLAUDE.md | 3 ++ .../tests/test_agent_llm_authority_docs.py | 30 ------------------- .../test_opencode_central_config_consumer.py | 6 ++++ docs/product-technical-gap-baseline.md | 7 +++++ 6 files changed, 22 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1ccdd17ea..6766545e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -690,8 +690,10 @@ in this repo. `codegraph init -i` autonomously without asking first; keep generated `.codegraph/` and `.cursor/rules/codegraph.mdc` artifacts local unless a future repository policy explicitly says to commit them. OpenCode PR review - uses the project `opencode.jsonc` MCP servers for CodeGraph, DeepWiki, - Context7, and web search. It must initialize CodeGraph before review so + receives the only `opencode.jsonc` from the central required workflow; do not + add repository-local `opencode.json` or `opencode.jsonc`. The central owner + registers review MCP servers, including Graphify. It must initialize + CodeGraph before review so structural findings cite graph-backed evidence instead of relying only on grep or raw file reads; use Context7 for current library docs, DeepWiki for repository documentation, and web search only for bounded external lookups. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec84c36f..83cb58dd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ## [Unreleased] +- 중앙 required workflow가 제공하는 단일 `opencode.jsonc`와 Graphify MCP를 + 소비하도록 저장소 로컬 OpenCode JSON을 제거하고 재도입 방지 계약을 둡니다. - 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다. - EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다. diff --git a/CLAUDE.md b/CLAUDE.md index 0bfd4f580..b067360d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,9 @@ merged by metadata-only robot governance (see `docs/development/merge-gate-policy.md`); OpenCode Review, Strix Security Scan, and the merge scheduler come from central workflows in `ContextualWisdomLab/.github` — do not reintroduce repo-local copies. +That owner also supplies the only `opencode.jsonc` used by required review; +Naruon must not add `opencode.json` or `opencode.jsonc`. Central review MCP +registration, including Graphify, stays owner-controlled. ## Common commands diff --git a/backend/tests/test_agent_llm_authority_docs.py b/backend/tests/test_agent_llm_authority_docs.py index e8dcc9c40..7104302a5 100644 --- a/backend/tests/test_agent_llm_authority_docs.py +++ b/backend/tests/test_agent_llm_authority_docs.py @@ -1,6 +1,5 @@ """Regression contracts for current LLM-routing authority guidance.""" -import json from pathlib import Path @@ -52,32 +51,3 @@ def test_agent_guidance_names_canonical_llm_owner_and_fail_closed_boundary() -> assert "provider discovery" in architecture assert "immutable released" in architecture assert "fails closed" in architecture - - -def test_opencode_config_uses_only_contextual_orchestrator_free() -> None: - """Repository OpenCode model work must use only the canonical logical pool.""" - raw_config = _read("opencode.jsonc") - config = json.loads(raw_config) - - assert config["model"] == "contextual-orchestrator/orchestrator/free" - assert config["small_model"] == "contextual-orchestrator/orchestrator/free" - assert config["enabled_providers"] == ["contextual-orchestrator"] - assert set(config["provider"]) == {"contextual-orchestrator"} - - provider = config["provider"]["contextual-orchestrator"] - assert provider["options"] == { - "baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}", - "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}", - } - assert set(provider["models"]) == {"orchestrator/free"} - - forbidden_direct_routing = ( - "github-models", - "STRIX_GITHUB_MODELS_TOKEN", - "https://models.github.ai/inference", - '"openai/gpt-5"', - '"deepseek/deepseek-r1-0528"', - '"deepseek/deepseek-v3-0324"', - ) - for phrase in forbidden_direct_routing: - assert phrase not in raw_config diff --git a/backend/tests/test_opencode_central_config_consumer.py b/backend/tests/test_opencode_central_config_consumer.py index 2e34e8771..32b20a186 100644 --- a/backend/tests/test_opencode_central_config_consumer.py +++ b/backend/tests/test_opencode_central_config_consumer.py @@ -14,3 +14,9 @@ def test_required_review_has_no_repository_local_opencode_configuration() -> Non "central OpenCode configuration must remain owner-controlled: " f"{relative_path}" ) + + agents = (REPO_ROOT / "AGENTS.md").read_text(encoding="utf-8") + claude = (REPO_ROOT / "CLAUDE.md").read_text(encoding="utf-8") + for guidance in (agents, claude): + assert "only `opencode.jsonc`" in guidance + assert "Graphify" in guidance diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 98bc17d2a..ec2bdfa81 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -6,6 +6,13 @@ **Observed product version:** `0.14.4` **Canonical completion issue:** [#1428](https://github.com/ContextualWisdomLab/naruon/issues/1428) +**Central OpenCode consumer boundary:** Draft PR +[#1625](https://github.com/ContextualWisdomLab/naruon/pull/1625) removes both +repository-local OpenCode JSON names and pins that absence in a focused +contract. Central owner PR [#2052](https://github.com/ContextualWisdomLab/.github/pull/2052) +remains the prerequisite for Graphify and the canonical `opencode.jsonc`; +neither open PR is protected-release or deployment evidence. + **Inventory observation:** the 106-PR open surface below is a fresh live scan captured at `2026-08-25T15:52:01Z`, which returned 106 open PRs after PR #1337 merged into protected `develop` at `2026-08-25T00:10:39Z` and From 9b19ceddf9f305f43e6016cebb46a90a61ad475f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 15:43:41 +0900 Subject: [PATCH 24/31] chore(review): restore canonical release and gap writers --- CHANGELOG.md | 2 -- docs/product-technical-gap-baseline.md | 7 ------- 2 files changed, 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83cb58dd6..7ec84c36f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,4 @@ ## [Unreleased] -- 중앙 required workflow가 제공하는 단일 `opencode.jsonc`와 Graphify MCP를 - 소비하도록 저장소 로컬 OpenCode JSON을 제거하고 재도입 방지 계약을 둡니다. - 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다. - EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ec2bdfa81..98bc17d2a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -6,13 +6,6 @@ **Observed product version:** `0.14.4` **Canonical completion issue:** [#1428](https://github.com/ContextualWisdomLab/naruon/issues/1428) -**Central OpenCode consumer boundary:** Draft PR -[#1625](https://github.com/ContextualWisdomLab/naruon/pull/1625) removes both -repository-local OpenCode JSON names and pins that absence in a focused -contract. Central owner PR [#2052](https://github.com/ContextualWisdomLab/.github/pull/2052) -remains the prerequisite for Graphify and the canonical `opencode.jsonc`; -neither open PR is protected-release or deployment evidence. - **Inventory observation:** the 106-PR open surface below is a fresh live scan captured at `2026-08-25T15:52:01Z`, which returned 106 open PRs after PR #1337 merged into protected `develop` at `2026-08-25T00:10:39Z` and From b97f42f16f2dfedccc1e98812c88117132ef8126 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:37:41 +0900 Subject: [PATCH 25/31] fix(deps): raise frontend audit security floors --- AGENTS.md | 6 + CLAUDE.md | 3 + .../test_frontend_framework_security_floor.py | 38 +++++ frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 139 +++++++++--------- frontend/pnpm-workspace.yaml | 1 + 6 files changed, 120 insertions(+), 71 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9104dd1f4..0557febfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,6 +130,12 @@ in this repo. them as source can exhaust model context before security evidence finalizes. - Prefer upgrading or removing vulnerable dependencies over downgrading patched packages unless compatibility evidence is recorded in the PR. +- Dependency-audit results are time-sensitive. Re-run the exact-head audit + immediately before push; a newly published advisory is a source repair, not + a reason to reuse an older clean receipt. After regenerating a pnpm lockfile, + require a warning-free frozen install and `pnpm peers check`. Do not adopt an + unsupported major or silence peer ranges merely to remove a deprecation + notice. - OpenCode Review, Strix Security Scan, and PR Review Merge Scheduler are provided by ContextualWisdomLab central required workflows in `ContextualWisdomLab/.github`; do not reintroduce repo-local copies of diff --git a/CLAUDE.md b/CLAUDE.md index be67bc80c..22227847c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,5 +174,8 @@ Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgve - Release bumps keep `VERSION`, `CHANGELOG.md`, `frontend/package.json`, and FastAPI app metadata synchronized; the backend reads its version from `VERSION`. +- Treat dependency audits as exact-head, time-sensitive evidence. After pnpm + lock regeneration, verify a warning-free frozen install and peer check; do + not hide unsupported-major peer conflicts to clear a deprecation notice. - When a review uncovers a recurring bug pattern, record the anti-pattern in `AGENTS.md` and update the affected tests/mocks/docs in the same PR. diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index bf318b5ad..5593b2304 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -15,6 +15,8 @@ FRONTEND_ROOT = REPO_ROOT / "frontend" NEXT_SECURITY_FLOOR = (16, 3, 3) SHARP_SECURITY_FLOOR = (0, 35, 4) +JS_YAML_SECURITY_FLOOR = (4, 3, 2) +VITEST_SECURITY_FLOOR = (4, 1, 11) def _exact_version(value: str) -> tuple[int, int, int]: @@ -139,6 +141,42 @@ def test_frontend_framework_and_image_security_floors() -> None: _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) +def test_js_yaml_security_floor_covers_every_lock_resolution() -> None: + """Keep every js-yaml resolution above the reviewed denial-of-service floor.""" + + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + for section_name in ("packages", "snapshots"): + js_yaml_keys = [ + key for key in lock[section_name] if key.startswith("js-yaml@") + ] + for package_key in js_yaml_keys: + assert ( + _package_key_version(package_key, "js-yaml") + >= JS_YAML_SECURITY_FLOOR + ), f"{section_name} contains js-yaml below the reviewed security floor" + + +def test_vitest_security_floor_covers_manifest_and_lock() -> None: + """Keep Vitest and its coverage package above the reviewed traversal floor.""" + + package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + for package_name in ("vitest", "@vitest/coverage-v8"): + declared_value = package["devDependencies"][package_name] + assert _exact_version(declared_value) >= VITEST_SECURITY_FLOOR + for section_name in ("packages", "snapshots"): + for package_key in lock[section_name]: + if package_key.startswith(f"{package_name}@"): + assert ( + _package_key_version(package_key, package_name) + >= VITEST_SECURITY_FLOOR + ), f"{section_name} contains {package_name} below the reviewed floor" + + @pytest.mark.parametrize("field", ["specifier", "version"]) def test_security_floor_rejects_root_importer_drift(field: str) -> None: """Reject a partially regenerated lock whose root Next.js importer drifts.""" diff --git a/frontend/package.json b/frontend/package.json index c86342aa1..f902cecd2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,14 +37,14 @@ "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", - "@vitest/coverage-v8": "4.1.10", + "@vitest/coverage-v8": "4.1.11", "eslint": "^9", "eslint-config-next": "16.3.4", "fast-check": "^4.9.0", "jsdom": "^30.0.1", "postcss": "8.5.24", "typescript": "^6", - "vitest": "^4.1.10" + "vitest": "4.1.11" }, "overrides": { "brace-expansion": "5.0.9", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index add535314..58377eb5f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: brace-expansion: 5.0.9 + js-yaml: 4.3.2 postcss: 8.5.24 sharp: 0.35.4 undici: 8.9.0 @@ -78,8 +79,8 @@ importers: specifier: ^19 version: 19.2.3(@types/react@19.2.17) '@vitest/coverage-v8': - specifier: 4.1.10 - version: 4.1.10(vitest@4.1.10) + specifier: 4.1.11 + version: 4.1.11(vitest@4.1.11) eslint: specifier: ^9 version: 9.39.5(jiti@2.7.0) @@ -99,8 +100,8 @@ importers: specifier: ^6 version: 6.0.3 vitest: - specifier: ^4.1.10 - version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) + specifier: 4.1.11 + version: 4.1.11(@types/node@26.1.2)(@vitest/coverage-v8@4.1.11)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) packages: @@ -311,8 +312,8 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.6': - resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + '@eslint/eslintrc@3.3.7': + resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.39.5': @@ -1205,20 +1206,20 @@ packages: cpu: [x64] os: [win32] - '@vitest/coverage-v8@4.1.10': - resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + '@vitest/coverage-v8@4.1.11': + resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} peerDependencies: - '@vitest/browser': 4.1.10 - vitest: 4.1.10 + '@vitest/browser': 4.1.11 + vitest: 4.1.11 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1228,20 +1229,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1987,8 +1988,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true jsdom@30.0.1: @@ -2808,20 +2809,20 @@ packages: yaml: optional: true - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3165,7 +3166,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.6': + '@eslint/eslintrc@3.3.7': dependencies: ajv: 6.15.0 debug: 4.4.3 @@ -3173,7 +3174,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.2 minimatch: 3.1.5(patch_hash=5f38b9c5382c1163b0389810f5e4e867519096f3c11a6df0a51d7cafbdfa93e2) strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -3870,10 +3871,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.10 + '@vitest/utils': 4.1.11 ast-v8-to-istanbul: 1.0.4 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -3882,46 +3883,46 @@ snapshots: obug: 2.1.3 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) + vitest: 4.1.11(@types/node@26.1.2)(@vitest/coverage-v8@4.1.11)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) - '@vitest/expect@4.1.10': + '@vitest/expect@4.1.11': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0))': + '@vitest/mocker@4.1.11(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0))': dependencies: - '@vitest/spy': 4.1.10 + '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.1.4(@types/node@26.1.2)(jiti@2.7.0) - '@vitest/pretty-format@4.1.10': + '@vitest/pretty-format@4.1.11': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.10': + '@vitest/runner@4.1.11': dependencies: - '@vitest/utils': 4.1.10 + '@vitest/utils': 4.1.11 pathe: 2.0.3 - '@vitest/snapshot@4.1.10': + '@vitest/snapshot@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.10': {} + '@vitest/spy@4.1.11': {} - '@vitest/utils@4.1.10': + '@vitest/utils@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 + '@vitest/pretty-format': 4.1.11 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -4312,7 +4313,7 @@ snapshots: '@next/eslint-plugin-next': 16.3.4(eslint@9.39.5(jiti@2.7.0)) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)) @@ -4335,7 +4336,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -4350,14 +4351,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -4372,7 +4373,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.5(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0)) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -4460,7 +4461,7 @@ snapshots: '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.6 + '@eslint/eslintrc': 3.3.7 '@eslint/js': 9.39.5 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.8 @@ -4846,7 +4847,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.3.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -5697,15 +5698,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)): + vitest@4.1.11(@types/node@26.1.2)(@vitest/coverage-v8@4.1.11)(jsdom@30.0.1)(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)): dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.1.4(@types/node@26.1.2)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 es-module-lexer: 2.3.0 expect-type: 1.4.0 magic-string: 0.30.21 @@ -5721,7 +5722,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.1.2 - '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) jsdom: 30.0.1 transitivePeerDependencies: - msw diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 784b361c1..70f2c4eea 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -15,6 +15,7 @@ supportedArchitectures: overrides: brace-expansion: "5.0.9" + js-yaml: "4.3.2" postcss: "8.5.24" sharp: "0.35.4" undici: 8.9.0 From 09cb87a25e59b0b5e737f915f77b404cafe245ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:44:15 +0900 Subject: [PATCH 26/31] test(deps): bind js-yaml owner succession structurally Preserve the concurrent generated 4.3.2/Vitest repair, remove AGENTS/CLAUDE writes from the dependency lane, and port the older js-yaml owner's useful workspace/lock/ESLint-consumer assertions into the protected-base security stack. This keeps one source owner without dropping the prior contract. --- AGENTS.md | 6 --- CLAUDE.md | 3 -- .../tests/test_js_yaml_dependency_security.py | 50 +++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 backend/tests/test_js_yaml_dependency_security.py diff --git a/AGENTS.md b/AGENTS.md index 0557febfd..9104dd1f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,12 +130,6 @@ in this repo. them as source can exhaust model context before security evidence finalizes. - Prefer upgrading or removing vulnerable dependencies over downgrading patched packages unless compatibility evidence is recorded in the PR. -- Dependency-audit results are time-sensitive. Re-run the exact-head audit - immediately before push; a newly published advisory is a source repair, not - a reason to reuse an older clean receipt. After regenerating a pnpm lockfile, - require a warning-free frozen install and `pnpm peers check`. Do not adopt an - unsupported major or silence peer ranges merely to remove a deprecation - notice. - OpenCode Review, Strix Security Scan, and PR Review Merge Scheduler are provided by ContextualWisdomLab central required workflows in `ContextualWisdomLab/.github`; do not reintroduce repo-local copies of diff --git a/CLAUDE.md b/CLAUDE.md index 22227847c..be67bc80c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,8 +174,5 @@ Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgve - Release bumps keep `VERSION`, `CHANGELOG.md`, `frontend/package.json`, and FastAPI app metadata synchronized; the backend reads its version from `VERSION`. -- Treat dependency audits as exact-head, time-sensitive evidence. After pnpm - lock regeneration, verify a warning-free frozen install and peer check; do - not hide unsupported-major peer conflicts to clear a deprecation notice. - When a review uncovers a recurring bug pattern, record the anti-pattern in `AGENTS.md` and update the affected tests/mocks/docs in the same PR. diff --git a/backend/tests/test_js_yaml_dependency_security.py b/backend/tests/test_js_yaml_dependency_security.py new file mode 100644 index 000000000..11f1bc3ad --- /dev/null +++ b/backend/tests/test_js_yaml_dependency_security.py @@ -0,0 +1,50 @@ +"""Keep the generated frontend dependency graph on the reviewed js-yaml floor.""" + +from pathlib import Path + +import yaml + + +FRONTEND_ROOT = Path(__file__).resolve().parents[2] / "frontend" +JS_YAML_PATCHED_RELEASE = "4.3.2" + + +def _resolved_version(package_key: str) -> tuple[int, int, int]: + """Return the semantic version from one peer-qualified js-yaml lock key.""" + + prefix = "js-yaml@" + assert package_key.startswith(prefix) + version = package_key[len(prefix) :].split("(", 1)[0] + return tuple(int(part) for part in version.split(".")) + + +def test_js_yaml_override_lock_and_eslint_consumer_share_patched_release() -> None: + """Bind workspace policy, generated lock identity, and the ESLint consumer together.""" + + workspace = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8") + ) + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + + assert str(workspace["overrides"]["js-yaml"]) == JS_YAML_PATCHED_RELEASE + assert str(lock["overrides"]["js-yaml"]) == JS_YAML_PATCHED_RELEASE + + floor = (4, 3, 2) + for section_name in ("packages", "snapshots"): + keys = [key for key in lock[section_name] if key.startswith("js-yaml@")] + assert keys, f"{section_name} must contain a js-yaml resolution" + assert {_resolved_version(key) for key in keys} == {floor} + + eslint_snapshots = [ + value + for key, value in lock["snapshots"].items() + if key.startswith("@eslint/eslintrc@") + ] + assert eslint_snapshots, "lock must retain the ESLint configuration snapshot" + assert any( + str(snapshot.get("dependencies", {}).get("js-yaml")) + == JS_YAML_PATCHED_RELEASE + for snapshot in eslint_snapshots + ), "ESLint must consume the reviewed js-yaml release" From a2759019b4b0f5c96605b43b0857197c3b6bfb19 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 18:14:12 +0900 Subject: [PATCH 27/31] docs(agents): preserve customer-copy boundary Signed-off-by: Seongho Bae --- AGENTS.md | 4 ++++ CLAUDE.md | 3 +++ 2 files changed, 7 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 1ccdd17ea..a31878c13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -580,6 +580,10 @@ in this repo. browser-submitted relationship classifications. Route layers should derive the thread provenance server-side, persist only scoped ontology metadata, and keep provider writes out of relationship capture. +- Relationship UI must derive customer wording from the bounded `next_action` + contract and must not render raw `action_reason`, provider rationale, source + keys, or machine action names. Unknown actions use a neutral next-step message; + browser coverage must assert both the customer text and absence of raw fields. - Unique email and forwarded-import dedupe must use strong scoped signals: normalized Message-ID, References/In-Reply-To, persisted duplicate provenance, or exact body/attachment fingerprints. Do not merge threads from subject-only diff --git a/CLAUDE.md b/CLAUDE.md index 0bfd4f580..fa0597bce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -184,6 +184,9 @@ proposed evidence. HTTP status from message substrings. Error responses stay honest and do not leak internals or credential-type details (see "Error-message contract" in `README.md`). +- Relationship panels map bounded `next_action` values to customer-owned copy. + Never display raw `action_reason`, provider rationale, source keys, or machine + action names; use a neutral fallback and assert raw-field absence in browser tests. - Never commit `.env`, real mailbox exports, or credentials; email fixtures in `backend/tests/fixtures` stay small and synthetic. - Release bumps keep `VERSION`, `CHANGELOG.md`, `frontend/package.json`, and From a6715c9c845ed5ad48929fd3261bb07ce4b802e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 04:41:41 +0900 Subject: [PATCH 28/31] test(deps): reproduce missing Vitest lock-entry gap --- .../test_frontend_framework_security_floor.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 5593b2304..f64edfe4a 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -177,6 +177,39 @@ def test_vitest_security_floor_covers_manifest_and_lock() -> None: ), f"{section_name} contains {package_name} below the reviewed floor" +@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) +@pytest.mark.parametrize("section_name", ["packages", "snapshots"]) +def test_vitest_security_floor_rejects_missing_lock_resolution( + monkeypatch: pytest.MonkeyPatch, + package_name: str, + section_name: str, +) -> None: + """Reject a regenerated lock section that drops an expected Vitest resolution.""" + + package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + lock[section_name] = { + key: value + for key, value in lock[section_name].items() + if not key.startswith(f"{package_name}@") + } + lock_text = yaml.safe_dump(lock) + original_read_text = Path.read_text + + def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: + if path == FRONTEND_ROOT / "package.json": + return package_text + if path == FRONTEND_ROOT / "pnpm-lock.yaml": + return lock_text + return original_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", _read_text) + with pytest.raises(AssertionError): + test_vitest_security_floor_covers_manifest_and_lock() + + @pytest.mark.parametrize("field", ["specifier", "version"]) def test_security_floor_rejects_root_importer_drift(field: str) -> None: """Reject a partially regenerated lock whose root Next.js importer drifts.""" From 21897d8ccd11ee4a2d7a3df5551db12c92c9ec71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 04:45:28 +0900 Subject: [PATCH 29/31] test(deps): require Vitest lock resolutions --- .../test_frontend_framework_security_floor.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index f64edfe4a..8c4457571 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -169,12 +169,19 @@ def test_vitest_security_floor_covers_manifest_and_lock() -> None: declared_value = package["devDependencies"][package_name] assert _exact_version(declared_value) >= VITEST_SECURITY_FLOOR for section_name in ("packages", "snapshots"): - for package_key in lock[section_name]: - if package_key.startswith(f"{package_name}@"): - assert ( - _package_key_version(package_key, package_name) - >= VITEST_SECURITY_FLOOR - ), f"{section_name} contains {package_name} below the reviewed floor" + package_keys = [ + package_key + for package_key in lock[section_name] + if package_key.startswith(f"{package_name}@") + ] + assert package_keys, ( + f"{section_name} must contain a {package_name} resolution" + ) + for package_key in package_keys: + assert ( + _package_key_version(package_key, package_name) + >= VITEST_SECURITY_FLOOR + ), f"{section_name} contains {package_name} below the reviewed floor" @pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) From 15fecaaeabe6faaedf7e4c3f8991eb3add1dd0c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 05:15:48 +0900 Subject: [PATCH 30/31] test(security): reproduce Vitest importer drift --- .../test_frontend_framework_security_floor.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 8c4457571..4d352fcb0 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -242,3 +242,64 @@ def test_security_floor_rejects_every_below_floor_lock_entry( with pytest.raises(AssertionError): _assert_lock_contract(lock, next_value, eslint_next_value, sharp_value) + + +@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) +@pytest.mark.parametrize("field", ["specifier", "version"]) +def test_vitest_security_floor_rejects_root_importer_drift( + monkeypatch: pytest.MonkeyPatch, + package_name: str, + field: str, +) -> None: + """Reject a root Vitest importer that no longer matches the reviewed manifest.""" + + package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + lock["importers"]["."]["devDependencies"][package_name][field] = "4.1.12" + lock_text = yaml.safe_dump(lock) + original_read_text = Path.read_text + + def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: + if path == FRONTEND_ROOT / "package.json": + return package_text + if path == FRONTEND_ROOT / "pnpm-lock.yaml": + return lock_text + return original_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", _read_text) + with pytest.raises(AssertionError): + test_vitest_security_floor_covers_manifest_and_lock() + + +@pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) +def test_vitest_security_floor_rejects_missing_root_snapshot( + monkeypatch: pytest.MonkeyPatch, + package_name: str, +) -> None: + """Reject a root Vitest resolution whose exact peer-qualified snapshot vanished.""" + + package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") + lock = yaml.safe_load( + (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") + ) + resolution = str( + lock["importers"]["."]["devDependencies"][package_name]["version"] + ) + snapshot_key = f"{package_name}@{resolution}" + snapshot = lock["snapshots"].pop(snapshot_key) + lock["snapshots"][f"{package_name}@4.1.12"] = snapshot + lock_text = yaml.safe_dump(lock) + original_read_text = Path.read_text + + def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: + if path == FRONTEND_ROOT / "package.json": + return package_text + if path == FRONTEND_ROOT / "pnpm-lock.yaml": + return lock_text + return original_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", _read_text) + with pytest.raises(AssertionError): + test_vitest_security_floor_covers_manifest_and_lock() From 17a7618eda2b212b691f08fa936e042b34258fc9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 05:18:45 +0900 Subject: [PATCH 31/31] test(security): bind Vitest importer contract --- .../tests/test_frontend_framework_security_floor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/tests/test_frontend_framework_security_floor.py b/backend/tests/test_frontend_framework_security_floor.py index 4d352fcb0..c1a7e2b9b 100644 --- a/backend/tests/test_frontend_framework_security_floor.py +++ b/backend/tests/test_frontend_framework_security_floor.py @@ -165,9 +165,20 @@ def test_vitest_security_floor_covers_manifest_and_lock() -> None: lock = yaml.safe_load( (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") ) + importer = lock["importers"]["."]["devDependencies"] for package_name in ("vitest", "@vitest/coverage-v8"): declared_value = package["devDependencies"][package_name] assert _exact_version(declared_value) >= VITEST_SECURITY_FLOOR + importer_entry = importer[package_name] + assert importer_entry["specifier"] == declared_value, ( + f"root importer must preserve the package.json {package_name} specifier" + ) + assert _resolved_version(str(importer_entry["version"])) == _exact_version( + declared_value + ), f"root importer must resolve the reviewed {package_name} release" + assert f"{package_name}@{importer_entry['version']}" in lock["snapshots"], ( + f"root importer {package_name} resolution must reference an existing snapshot" + ) for section_name in ("packages", "snapshots"): package_keys = [ package_key