From aff24ea06645047ad9721b88115e30cfcc345eab Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 15 Oct 2026 02:18:59 +0800 Subject: [PATCH 1/2] docs(gov-court): converge governance artifacts to final state - Retire dead material by deletion (no snapshots): .scratch 441->62MB, .codex-tmp 182->106 files, docs/history + 15 dead taskbooks + phase-history + DOCUMENT_GOVERNANCE_PLAN + 26 bookkeeping ADRs removed - AGENTS.md slimmed to C.4 skeleton (32.7KB->1.7KB); product conventions moved to CONTRIBUTING.md (authoritative), agent ops rules to docs/agents/README.md; resolves AGENTS push-clause conflict (L-5) - Retire all 5 governance gates: instrument-declaration-check, AGENTS-32KiB, strategy_service-4000, snapshot-2000, docs-lint.yml + markdownlint config; register rows marked RETIRED with residual notes - Fix ADR status fields (0053/0078/0079); mark retired-ADR citations in CONTEXT/storage-locations/ARCHITECTURE/PONYTAIL/glossary/research/matrix Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/docs-lint.yml | 98 --- .markdownlint-cli2.jsonc | 30 - AGENTS.md | 235 +---- CONTEXT.md | 2 +- CONTRIBUTING.md | 15 +- docs/DOCUMENT_GOVERNANCE_PLAN.md | 368 -------- docs/PONYTAIL_DEBT_LEDGER.md | 6 +- docs/README.md | 8 +- ...01-shell-forwarding-vs-resin-webui-port.md | 125 --- docs/adr/0002-topology-canvas-three-column.md | 57 -- docs/adr/0004-egress-policy-surface.md | 49 -- docs/adr/0005-q5-q9-cluster.md | 127 --- docs/adr/0006-mainline-a-then-b.md | 62 -- .../0007-subscription-last-error-surface.md | 77 -- docs/adr/0008-item3-live-sidecar-e2e.md | 139 --- docs/adr/0009-cli-gui-alignment-status.md | 124 --- ...ild-all-ps1-and-release-standardization.md | 37 - .../0020-test-per-capability-closed-loop.md | 37 - docs/adr/0023-node-pool-tree-view.md | 21 - docs/adr/0025-topology-dynamic-pool-edges.md | 28 - docs/adr/0029-platform-page-ux-fix.md | 29 - docs/adr/0030-cross-platform-build-compat.md | 27 - docs/adr/0031-platform-gui-refactor.md | 40 - docs/adr/0032-platform-page-ux-polish.md | 68 -- docs/adr/0033-platform-page-ux-fix-t12.md | 25 - .../0034-canvas-v2-dagre-strategy-layout.md | 36 - docs/adr/0038-t15-perf-fixes.md | 30 - ...039-canvas-v3-fold-center-strategy-sync.md | 52 -- ...-canvas-region-filter-port-bind-minimap.md | 70 -- ...0041-canvas-v4-node-pool-toolbars-merge.md | 117 --- ...ol-refresh-batch-progress-button-layout.md | 59 -- .../0048-canvas-a-class-semantic-filtering.md | 60 -- docs/adr/0053-tauri-specta-adoption-pilot.md | 2 +- .../adr/0075-canvas-write-read-model-fault.md | 107 --- ...anvas-node-below-platform-three-defects.md | 87 -- ...cceptance-line-dual-deployment-profiles.md | 2 +- .../0079-resin-positioning-value-boundary.md | 2 +- docs/agents/README.md | 50 +- docs/agents/phase-history.md | 805 ------------------ docs/agents/round11-next-round.md | 148 ---- docs/agents/round11-wave-b-next-round.md | 89 -- docs/agents/round11-wave-c-next-round.md | 99 --- docs/agents/round12-next-round.md | 68 -- docs/agents/round12-wave-a-next-round.md | 68 -- docs/agents/round12-wave-b-next-round.md | 40 - docs/agents/round12-wave-c-next-round.md | 62 -- docs/agents/round12-wave-d-next-round.md | 117 --- docs/agents/round12-wave-e-next-round.md | 92 -- docs/agents/round12-wave-f-next-round.md | 70 -- docs/agents/round12-wave-g-next-round.md | 86 -- docs/agents/round12-wave-h-next-round.md | 57 -- docs/agents/round12-wave-i-next-round.md | 66 -- docs/agents/round12-wave-j-next-round.md | 57 -- docs/agents/round12-wave-k-next-round.md | 52 -- docs/agents/round9-next-round.md | 137 --- docs/agents/storage-locations.md | 2 +- docs/agents/trigger-line-register.md | 10 +- docs/architecture/ARCHITECTURE.md | 2 +- docs/history/README.md | 18 - docs/history/handoffs/HANDOFF_PATH_A.md | 145 ---- .../handoffs/HANDOFF_PATH_A_CONTINUATION.md | 77 -- docs/history/handoffs/HANDOFF_POLISH_PHASE.md | 81 -- .../handoffs/HANDOFF_ROUTE_CORRECTION.md | 83 -- .../handoffs/HANDOFF_T2_LIFECYCLE_PIPELINE.md | 45 - docs/history/handoffs/HANDOFF_T3_REMAINING.md | 170 ---- .../handoffs/HANDOFF_T4_GRILL_COMPLETE.md | 38 - docs/history/handoffs/HANDOFF_T5_TRACE_IPC.md | 124 --- .../handoffs/HANDOFF_T6_NETWORK_LAYER.md | 51 -- .../handoffs/HANDOFF_T8_EXIT_IP_STRATEGY.md | 62 -- .../HANDOFF_T9_CROSS_PLATFORM_BUILD.md | 48 -- docs/history/phases/GRILL_ISSUES_BACKLOG.md | 164 ---- .../phases/GRILL_T10_PLATFORM_GUI_REFACTOR.md | 92 -- .../GRILL_T11_PLATFORM_PAGE_UX_POLISH.md | 87 -- .../phases/GRILL_T12_PLATFORM_PAGE_FIX.md | 111 --- .../phases/GRILL_T13_CANVAS_V2_PLAN.md | 114 --- .../phases/GRILL_T14_CANVAS_BUGFIX_PLAN.md | 85 -- docs/history/phases/GRILL_T14_PERFORMANCE.md | 143 ---- .../phases/GRILL_T15_CANVAS_FIX_PLAN.md | 108 --- .../phases/GRILL_T15_CANVAS_V3_PLAN.md | 111 --- .../phases/GRILL_T15_PERFORMANCE_FIX.md | 118 --- .../GRILL_T16_CANVAS_REGION_FILTER_PLAN.md | 53 -- .../phases/GRILL_T16_PORT_IDENTITY_PLAN.md | 64 -- .../phases/GRILL_T17_CANVAS_V4_PLAN.md | 109 --- .../phases/GRILL_T17_HEADLESS_SERVER.md | 99 --- .../GRILL_T18_CANVAS_PIPELINE_SYNC_PLAN.md | 148 ---- .../phases/GRILL_T19_NODE_POOL_FIX_PLAN.md | 144 ---- .../phases/GRILL_T20_IPC_ERROR_CONTRACT.md | 90 -- ...LL_T20_SUBSCRIPTION_REFRESH_NATIVE_PLAN.md | 210 ----- .../GRILL_T21_NODE_POOL_BUTTON_SYNC_PLAN.md | 164 ---- .../GRILL_T22_CANVAS_A_CLASS_SEMANTIC_PLAN.md | 112 --- .../GRILL_T22_HEADLESS_RENDERING_FIX.md | 106 --- docs/history/phases/GRILL_T4_BACKLOG.md | 71 -- docs/history/phases/GRILL_T5_DOMAIN_AUDIT.md | 105 --- .../phases/GRILL_T5_TRACE_IPC_BACKLOG.md | 91 -- .../phases/GRILL_T6_NETWORK_LAYER_PLAN.md | 119 --- docs/history/phases/GRILL_T6_PLAN.md | 139 --- .../phases/GRILL_T7_DIAGNOSTICS_PLAN.md | 180 ---- .../phases/GRILL_T8_EXIT_IP_STRATEGY_PLAN.md | 196 ----- .../phases/GRILL_T8_PLATFORM_PAGE_FIX.md | 50 -- .../phases/GRILL_T9_CANVAS_FIX_PLAN.md | 116 --- .../phases/GRILL_T9_CROSS_PLATFORM_BUILD.md | 130 --- .../history/phases/PONYTAIL_DEBT_T16_AUDIT.md | 18 - docs/history/phases/PROJECT_PLAN.md | 22 - docs/history/phases/T4_EXECUTION_PLAN.md | 109 --- .../phases/T7-TEST-NETWORK-DIAGNOSIS.md | 135 --- docs/history/prompts/T9_MASTER_GOAL_PROMPT.md | 38 - docs/history/prompts/USER_PROMPT_ARCHIVE.md | 78 -- docs/reference/CAPABILITY_MATRIX.md | 14 +- docs/reference/glossary.md | 6 +- .../RESIN_ROUTING_ARCHITECTURE_RESEARCH.md | 2 +- scripts/instrument-declaration-check.cjs | 150 ---- scripts/verify-build.sh | 38 - 112 files changed, 99 insertions(+), 9717 deletions(-) delete mode 100644 .github/workflows/docs-lint.yml delete mode 100644 .markdownlint-cli2.jsonc delete mode 100644 docs/DOCUMENT_GOVERNANCE_PLAN.md delete mode 100644 docs/adr/0001-shell-forwarding-vs-resin-webui-port.md delete mode 100644 docs/adr/0002-topology-canvas-three-column.md delete mode 100644 docs/adr/0004-egress-policy-surface.md delete mode 100644 docs/adr/0005-q5-q9-cluster.md delete mode 100644 docs/adr/0006-mainline-a-then-b.md delete mode 100644 docs/adr/0007-subscription-last-error-surface.md delete mode 100644 docs/adr/0008-item3-live-sidecar-e2e.md delete mode 100644 docs/adr/0009-cli-gui-alignment-status.md delete mode 100644 docs/adr/0019-build-all-ps1-and-release-standardization.md delete mode 100644 docs/adr/0020-test-per-capability-closed-loop.md delete mode 100644 docs/adr/0023-node-pool-tree-view.md delete mode 100644 docs/adr/0025-topology-dynamic-pool-edges.md delete mode 100644 docs/adr/0029-platform-page-ux-fix.md delete mode 100644 docs/adr/0030-cross-platform-build-compat.md delete mode 100644 docs/adr/0031-platform-gui-refactor.md delete mode 100644 docs/adr/0032-platform-page-ux-polish.md delete mode 100644 docs/adr/0033-platform-page-ux-fix-t12.md delete mode 100644 docs/adr/0034-canvas-v2-dagre-strategy-layout.md delete mode 100644 docs/adr/0038-t15-perf-fixes.md delete mode 100644 docs/adr/0039-canvas-v3-fold-center-strategy-sync.md delete mode 100644 docs/adr/0040-canvas-region-filter-port-bind-minimap.md delete mode 100644 docs/adr/0041-canvas-v4-node-pool-toolbars-merge.md delete mode 100644 docs/adr/0046-node-pool-refresh-batch-progress-button-layout.md delete mode 100644 docs/adr/0048-canvas-a-class-semantic-filtering.md delete mode 100644 docs/adr/0075-canvas-write-read-model-fault.md delete mode 100644 docs/adr/0077-canvas-node-below-platform-three-defects.md delete mode 100644 docs/agents/phase-history.md delete mode 100644 docs/agents/round11-next-round.md delete mode 100644 docs/agents/round11-wave-b-next-round.md delete mode 100644 docs/agents/round11-wave-c-next-round.md delete mode 100644 docs/agents/round12-next-round.md delete mode 100644 docs/agents/round12-wave-a-next-round.md delete mode 100644 docs/agents/round12-wave-b-next-round.md delete mode 100644 docs/agents/round12-wave-c-next-round.md delete mode 100644 docs/agents/round12-wave-d-next-round.md delete mode 100644 docs/agents/round12-wave-e-next-round.md delete mode 100644 docs/agents/round12-wave-f-next-round.md delete mode 100644 docs/agents/round12-wave-g-next-round.md delete mode 100644 docs/agents/round12-wave-h-next-round.md delete mode 100644 docs/agents/round12-wave-i-next-round.md delete mode 100644 docs/agents/round12-wave-j-next-round.md delete mode 100644 docs/agents/round12-wave-k-next-round.md delete mode 100644 docs/agents/round9-next-round.md delete mode 100644 docs/history/README.md delete mode 100644 docs/history/handoffs/HANDOFF_PATH_A.md delete mode 100644 docs/history/handoffs/HANDOFF_PATH_A_CONTINUATION.md delete mode 100644 docs/history/handoffs/HANDOFF_POLISH_PHASE.md delete mode 100644 docs/history/handoffs/HANDOFF_ROUTE_CORRECTION.md delete mode 100644 docs/history/handoffs/HANDOFF_T2_LIFECYCLE_PIPELINE.md delete mode 100644 docs/history/handoffs/HANDOFF_T3_REMAINING.md delete mode 100644 docs/history/handoffs/HANDOFF_T4_GRILL_COMPLETE.md delete mode 100644 docs/history/handoffs/HANDOFF_T5_TRACE_IPC.md delete mode 100644 docs/history/handoffs/HANDOFF_T6_NETWORK_LAYER.md delete mode 100644 docs/history/handoffs/HANDOFF_T8_EXIT_IP_STRATEGY.md delete mode 100644 docs/history/handoffs/HANDOFF_T9_CROSS_PLATFORM_BUILD.md delete mode 100644 docs/history/phases/GRILL_ISSUES_BACKLOG.md delete mode 100644 docs/history/phases/GRILL_T10_PLATFORM_GUI_REFACTOR.md delete mode 100644 docs/history/phases/GRILL_T11_PLATFORM_PAGE_UX_POLISH.md delete mode 100644 docs/history/phases/GRILL_T12_PLATFORM_PAGE_FIX.md delete mode 100644 docs/history/phases/GRILL_T13_CANVAS_V2_PLAN.md delete mode 100644 docs/history/phases/GRILL_T14_CANVAS_BUGFIX_PLAN.md delete mode 100644 docs/history/phases/GRILL_T14_PERFORMANCE.md delete mode 100644 docs/history/phases/GRILL_T15_CANVAS_FIX_PLAN.md delete mode 100644 docs/history/phases/GRILL_T15_CANVAS_V3_PLAN.md delete mode 100644 docs/history/phases/GRILL_T15_PERFORMANCE_FIX.md delete mode 100644 docs/history/phases/GRILL_T16_CANVAS_REGION_FILTER_PLAN.md delete mode 100644 docs/history/phases/GRILL_T16_PORT_IDENTITY_PLAN.md delete mode 100644 docs/history/phases/GRILL_T17_CANVAS_V4_PLAN.md delete mode 100644 docs/history/phases/GRILL_T17_HEADLESS_SERVER.md delete mode 100644 docs/history/phases/GRILL_T18_CANVAS_PIPELINE_SYNC_PLAN.md delete mode 100644 docs/history/phases/GRILL_T19_NODE_POOL_FIX_PLAN.md delete mode 100644 docs/history/phases/GRILL_T20_IPC_ERROR_CONTRACT.md delete mode 100644 docs/history/phases/GRILL_T20_SUBSCRIPTION_REFRESH_NATIVE_PLAN.md delete mode 100644 docs/history/phases/GRILL_T21_NODE_POOL_BUTTON_SYNC_PLAN.md delete mode 100644 docs/history/phases/GRILL_T22_CANVAS_A_CLASS_SEMANTIC_PLAN.md delete mode 100644 docs/history/phases/GRILL_T22_HEADLESS_RENDERING_FIX.md delete mode 100644 docs/history/phases/GRILL_T4_BACKLOG.md delete mode 100644 docs/history/phases/GRILL_T5_DOMAIN_AUDIT.md delete mode 100644 docs/history/phases/GRILL_T5_TRACE_IPC_BACKLOG.md delete mode 100644 docs/history/phases/GRILL_T6_NETWORK_LAYER_PLAN.md delete mode 100644 docs/history/phases/GRILL_T6_PLAN.md delete mode 100644 docs/history/phases/GRILL_T7_DIAGNOSTICS_PLAN.md delete mode 100644 docs/history/phases/GRILL_T8_EXIT_IP_STRATEGY_PLAN.md delete mode 100644 docs/history/phases/GRILL_T8_PLATFORM_PAGE_FIX.md delete mode 100644 docs/history/phases/GRILL_T9_CANVAS_FIX_PLAN.md delete mode 100644 docs/history/phases/GRILL_T9_CROSS_PLATFORM_BUILD.md delete mode 100644 docs/history/phases/PONYTAIL_DEBT_T16_AUDIT.md delete mode 100644 docs/history/phases/PROJECT_PLAN.md delete mode 100644 docs/history/phases/T4_EXECUTION_PLAN.md delete mode 100644 docs/history/phases/T7-TEST-NETWORK-DIAGNOSIS.md delete mode 100644 docs/history/prompts/T9_MASTER_GOAL_PROMPT.md delete mode 100644 docs/history/prompts/USER_PROMPT_ARCHIVE.md delete mode 100644 scripts/instrument-declaration-check.cjs diff --git a/.github/workflows/docs-lint.yml b/.github/workflows/docs-lint.yml deleted file mode 100644 index 2d831f17..00000000 --- a/.github/workflows/docs-lint.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Docs Governance - -on: - workflow_dispatch: - pull_request: - paths: - - 'AGENTS.md' - - 'docs/**' - - 'CHANGELOG.md' - - 'CONTRIBUTING.md' - - 'SECURITY.md' - - 'CODE_OF_CONDUCT.md' - - 'PULL_REQUEST_TEMPLATE.md' - - '.github/ISSUE_TEMPLATE/**' - - '.github/workflows/docs-lint.yml' - push: - branches: [main, master] - paths: - - 'AGENTS.md' - - 'docs/**' - - 'CHANGELOG.md' - - 'CONTRIBUTING.md' - - 'SECURITY.md' - - 'CODE_OF_CONDUCT.md' - - 'PULL_REQUEST_TEMPLATE.md' - - '.github/ISSUE_TEMPLATE/**' - - '.github/workflows/docs-lint.yml' - -jobs: - governance: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Markdown lint (markdownlint-cli2) - uses: DavidAnson/markdownlint-cli2-action@v17 - with: - globs: | - AGENTS.md - README.md - README_CN.md - CHANGELOG.md - CONTRIBUTING.md - SECURITY.md - CODE_OF_CONDUCT.md - PULL_REQUEST_TEMPLATE.md - .github/ISSUE_TEMPLATE/*.md - docs/**/*.md - configPath: .markdownlint-cli2.jsonc - - - name: Dead-link check (docs/ references in AGENTS.md, README and community files) - run: | - set -e - errors=0 - # Extract docs/ path references from AGENTS.md, README.md, README_CN.md, - # CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, PULL_REQUEST_TEMPLATE.md, - # .github/ISSUE_TEMPLATE/*.md - for ref in $(grep -ohE 'docs/[a-zA-Z0-9_/]+\.md' AGENTS.md README.md README_CN.md CONTRIBUTING.md SECURITY.md CODE_OF_CONDUCT.md PULL_REQUEST_TEMPLATE.md .github/ISSUE_TEMPLATE/*.md 2>/dev/null | sort -u); do - if [ ! -f "$ref" ]; then - echo "::error::Dead link: $ref (referenced in AGENTS.md or README)" - errors=$((errors + 1)) - else - echo "OK: $ref" - fi - done - if [ "$errors" -gt 0 ]; then - echo "::error::$errors dead link(s) found in docs references" - exit 1 - fi - - - name: Layer separation guard (phase/handoff files not at docs/ top level) - run: | - set -e - violations=0 - # Files matching phase/handoff patterns must not be at docs/ top level - for f in docs/HANDOFF_*.md docs/GRILL_*.md docs/T[0-9]_*.md docs/PROJECT_PLAN.md docs/USER_PROMPT_ARCHIVE.md; do - if [ -f "$f" ]; then - echo "::error::Layer violation: $f should be in docs/history/" - violations=$((violations + 1)) - fi - done - if [ "$violations" -gt 0 ]; then - echo "::error::$violations layer separation violation(s) found" - exit 1 - fi - echo "Layer separation OK: no phase/handoff files at docs/ top level" - - - name: AGENTS.md size guard - run: | - lines=$(wc -l < AGENTS.md) - bytes=$(wc -c < AGENTS.md) - echo "AGENTS.md: $lines lines, $bytes bytes" - if [ "$bytes" -gt 32768 ]; then - echo "::warning::AGENTS.md exceeds Codex default 32 KiB limit ($bytes bytes). Configure project_doc_max_bytes or slim to docs/" - fi - if [ "$lines" -gt 500 ]; then - echo "::warning::AGENTS.md is $lines lines (>500) — consider migrating detail to docs/architecture/" - fi diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc deleted file mode 100644 index 0ecb5815..00000000 --- a/.markdownlint-cli2.jsonc +++ /dev/null @@ -1,30 +0,0 @@ -{ - // markdownlint-cli2 configuration for EgressAPIKEY - // https://github.com/DavidAnson/markdownlint-cli2 - "config": { - "default": true, - // Line length: tables and URLs are long by nature - "MD013": false, - // First line in file should be a top level heading: docs/README.md uses this - "MD041": false, - // Multiple top level headings: ADRs and phase docs have this - "MD025": false, - // Inline HTML: AGENTS.md and docs use raw HTML sparingly - "MD033": false, - // Duplicate headings: flag same-parent duplicates only. CHANGELOG.md repeats - // "Added/Changed/Fixed/Removed" per release under different "## [version]" parents - // (Keep a Changelog 2.0.0 format); renaming them would lose the standard shape. - // Matches pinned markdownlint-cli2 0.14.0 (docs-lint action v17). - "MD024": { "siblings_only": true } - }, - "ignores": [ - "node_modules/**", - "target/**", - "dist/**", - ".codegraph/**", - "docs/history/**", - "docs/adr/**", - // LICENSE is the official GPL-3.0 verbatim text - never lint it as markdown - "LICENSE" - ] -} diff --git a/AGENTS.md b/AGENTS.md index 12191417..b63f6f70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,227 +1,26 @@ -# EgressAPIKEY - Project Operating Instructions +# EgressAPIKEY — Agent Instructions -> **[!IMPORTANT] Folder rename complete**: the on-disk repository folder has been renamed from `D:\Aworker\ai-api-route` to `D:\Aworker\EgressAPIKEY`. All path-agnostic artifacts are aligned: `tauri.conf.json` productName = "EgressAPIKEY", Cargo bin name "egressapikey-app", npm name "EgressAPIKEY", bundle id "com.egressapikey.desktop". +EgressAPIKEY is a Tauri 2 + React 19 desktop app: an L7 proxy gateway specialized for AI API keys. Each key maps to a Resin (Platform, Account) pair; the vendored Resin Go sidecar guarantees a distinct sticky exit IP per pair. ---- +## Run -## context-mode routing (MANDATORY) +- Dev: `pnpm install` → `bash scripts/fetch_resin.sh` → `pnpm tauri dev` — full setup in [CONTRIBUTING.md](CONTRIBUTING.md) +- Verify: `bash scripts/verify-build.sh` — the same script CI runs +- Headless deployment: `docs/how-to/HEADLESS_DEPLOYMENT.md` +- Delivery builds are **CI-only** (ADR-0072): local compile/test/packaging is never delivery evidence -- File edits (including patches) MUST go through ctx_batch_execute / ctx_execute_file, not apply_patch. -- `ctx_*` first; fallback to Codex builtins only when `ctx_*` can't do the same job. Read-to-analyze / search / large grep: ctx_batch_execute(commands, queries) or ctx_search(queries) - never Get-Content/Select-String into context. For data analysis use ctx_execute(code) and print only the answer. -- Web/HTTP: ctx_fetch_and_index(url, source) then ctx_search(queries). curl/wget/inline HTTP are forbidden. -- Shell OK for git, mkdir, rm, mv, cd, ls, npm install, dotnet build, cargo build, vitest, scripts/build-all.ps1 (execution, not analysis; output is bounded and acceptable). -- Windows paths in ctx sandbox: use forward-slash Windows form `D:/Aworker/EgressAPIKEY/...` for both the `cwd` argument and inline paths. The ctx `shell` language routes to `pwsh.exe` (PowerShell 7), NOT bash — Git-Bash form `/d/Aworker/...` either resolves to the wrong drive `D:\d\Aworker\...` or, when passed as `cwd`, kills the spawn with `pwsh.exe ENOENT`. PowerShell cmdlets need `pwsh -NoProfile -Command "..."`. `$`-using PowerShell logic must go in a `.ps1` and run with `-File` (inline `$` is stripped by the host transport). -- After resume: `ctx_search(sort:"timeline")` before asking the user anything. Search prior session memory before re-reading sources. -- Output artifacts as files + path + one-line summary; never inline large content. Descriptive source labels for `ctx_search(source:"label")`. -- Keep this block at the very top. Any later agent editing this file must keep the context-mode routing block intact and on top. Extended project spec follows. +## Stack ---- +Rust core `crates/resin-core` (tokio, reqwest, rusqlite) · Tauri 2 shell `src-tauri` · frontend `src/` (React 19, TypeScript, Vite, Tailwind, ReactFlow 12, Zustand 5, react-i18next ×18 locales) · vendored Resin Go sidecar -## CodeGraph (MANDATORY for code exploration) +## Layout -CodeGraph is the project's indexed code intelligence layer. The index lives at `.codegraph/` (gitignored). All agents and LLMs working on this project MUST use CodeGraph as the FIRST step for code exploration — it returns verbatim source of relevant symbols grouped by file in one capped call, far more efficient than manual Grep/Read loops. +`crates/resin-core/` backend core · `src-tauri/` Tauri shell · `src/` frontend · `docs/` documentation · `scripts/` build + CI scripts · `e2e/` Playwright suites · `.scratch/` + `.codex-tmp/` are gitignored session-local dirs — never commit them -**How to use**: +## Pointers -- Via MCP: call `codegraph_explore` with `projectPath: "D:\Aworker\EgressAPIKEY"` and a query (symbol names, file names, or natural-language question). -- Via CLI: `codegraph explore ""` or `codegraph query ""` or `codegraph node ` or `codegraph files`. -- After any code change: run `codegraph sync .` to incrementally update the index. For a full rebuild: `codegraph index .`. -- Check index status: `codegraph status .`. - -**When to call FIRST (before reading files)**: - -- "How does X work?" or "Where is X defined?" -- "What calls Y?" or "What is the blast radius of changing Z?" -- Surveying an area before an edit -- Finding the call path between symbols - -**When NOT needed**: trivial one-file edits where you already know the exact line, or after CodeGraph has already returned the source in this session (treat returned source as already Read — do NOT re-open those files). - -**Index sync is recommended after code changes** (run `codegraph sync .` in the same session that changes code, so the index reflects it). This is a local developer aid, not a gate: the index is gitignored and never committed, and CI installs no codegraph and holds no index, so nothing in the push gate can enforce it (ticket 06 / spec IMP-6 #7 corrected the former hard-close-loop wording). - -## Project Overview - -EgressAPIKEY is a Tauri 2 + React 19 desktop app: an L7 proxy gateway specialized for AI API keys. Each key maps to a Resin (Platform, Account) pair; the Resin Go sidecar guarantees a distinct sticky exit IP per pair and locks the lease until an SSE stream completes. The Rust crates/resin-core is the shell-side support crate (loopback REST client, whitebox config store, port forwarder/health, strategy engine, stream sensing, IP reputation, shared throttle model, typed IPC errors) after the ADR-0050 dead-kernel-face deletion; it no longer re-implements the gateway kernel. - -- Backend core: `crates/resin-core/` (Rust: tokio, reqwest, rusqlite; axum dep removed with ADR-0050) -- Desktop shell: `src-tauri/` (Tauri 2, sidecar lifecycle, system tray, Ghost safety net) -- Frontend: `src/` (React 19, TS, Vite, Tailwind, ReactFlow 12, Zustand 5, react-i18next) -- Docs: `docs/architecture/ARCHITECTURE.md`, `docs/history/phases/PROJECT_PLAN.md`, `docs/architecture/MEMORY_REUSE_DECISION.md` -- Release artifacts: `release/` (gitignored except tags) -- README homepage: `README.md` is the EN-canonical front page (`README_CN.md` is the CN mirror, kept bilingual-gated). Its section skeleton, dynamic-only badges (license → `LICENSE`, CI → `.github/workflows/ci.yml`, release → Releases), download-first ordering, hero `` (light/dark), `## Architecture` section, screenshot placeholders, and compliance NOTE follow architecture-recovery spec §4 D-04/D-05/D-10/D-C3.1 — preserve the skeleton order on edit, route deep content to `docs/` instead of duplicating it, and keep the top language-switch line linked to the CN mirror. Visual assets live in `assets/readme/` (`hero.svg` + `hero-dark.svg` self-produced, `architecture.svg` rendered from the Mermaid block that stays embedded in BOTH READMEs, `topology.png`/`platforms.png`/`effective-config.png` = user-provided screenshots — never AI-fabricated; PNG slots are HTML comments until real captures land). The `## License` section body must stay exactly `GPL-3.0-or-later` (machine-checked by `scripts/license-field-check.cjs`), the Resin wording carries the two-layer license value + `THIRD_PARTY.md` pointer (§11), and removed tech (axum / mihomo, ADR-0050) must not reappear. The CN mirror is structure-locked to this skeleton by `scripts/readme-lang-check.cjs` (15 checks since ticket 10: skeleton + License anchors + cross-links + D-05 tokens + first-line `` commit-hash comments + 6-asset mirror coverage) — update both sides, the sync comments, and its mapping table together. The Documentation table on both homepages is itself gated by `scripts/readme-lang-check.cjs` check 9 (ticket 14): `docs/architecture/UPSTREAM.md` (the upstream four-pack router) and `docs/RELEASE_NOTES.md` (per-release notes with compatibility + upgrade notes, machine-aligned to CHANGELOG.md versions/dates by `scripts/upstream-router-check.cjs`, mounted in `scripts/verify-build.sh`) must stay linked from both front doors. - ---- - -## Storage locations (runtime) - -The app stores user data in OS-standard dirs (resolved by Tauri's `app.path()`). On Windows these land under `%APPDATA%`; on macOS under `~/Library/Application Support`; on Linux under `~/.config` / `~/.local/share`. The authoritative three-layer model is legislated in `docs/architecture/ARCHITECTURE.md` §「配置权威」(Config Authority); the layers here are L1 GUI preferences / L2 whitebox user-editable config / L3 Resin runtime — an agent must not add a config key without classifying it into one of the three. - -- **L1 `settings.json`** (`app_config_dir()`): GUI preferences (lang/theme/view, credentials, poll intervals); never influences proxy behavior. -- **L2 whitebox**: `egressapikey-strategy.json` + `egressapikey-ports.json` (+ `egressapikey.db` SQLite sync partner); written only via `resin_core::StrategyService` / `WhiteboxConfigStore` with backup rings + audit rows. -- **L3 Resin runtime**: sidecar state + live leases/listeners - execute-only via ResinClient, rebuildable from L2. - -Full per-layer detail (key inventory, write entries, backup rings, Mode A/B dataplane, audit log): `docs/agents/storage-locations.md`. - -## /init conventions (enforced from P0) - -Any agent or human landing on this repo MUST apply these conventions. Violating any is a blocking review comment. - -### 1. Code exploration - CodeGraph MANDATORY - -- Before reading source files to answer "how does X work / where is X / what calls Y", query CodeGraph first. See the CodeGraph block above for commands. -- After any source change in a commit, run `codegraph sync .` in the SAME session before committing so the index reflects the change. Recommended, not enforced: the index lives in `.codegraph/` (gitignored, never committed) and CI has no codegraph install, so the push gate cannot check it. -- Treat codegraph-returned source as already-Read; do NOT re-open those files in the same session. - -### 2. Tool routing - context-mode MANDATORY - -- See the context-mode routing block at the top. File edits, large grep, web fetch, data analysis all go through ctx_* first. `curl`/`wget`/inline HTTP are forbidden; use `ctx_fetch_and_index`. Shell is OK for bounded mutating commands (git, mkdir, cargo build, pnpm scripts). - -### 3. i18n - decoupled, full-key coverage - -- All user-visible strings in `src/` MUST come from the i18n catalog (`src/locales//*.json`) via `react-i18next` `t()` / `Trans`. Never hard-code English (or any locale) in components. -- Base locales (18): `en`, `zh`, `ja`, `es`, `fr`, `de`, `ko`, `ru`, `pt`, `ar`, `hi`, `id`, `it`, `nl`, `pl`, `th`, `tr`, `vi`. Adding a string means adding the key to ALL base locales in the same commit. The `Locale` union in `src/store/appStore.ts` and the `ALL` list in `scripts/i18n-check.cjs` MUST stay in lockstep with `src/locales/` directories. -- `pnpm i18n:scan` runs `i18next-parser` (config `i18next-parser.config.js`) to extract t()/Trans keys into `src/locales/`; `pnpm i18n:check` fails the build if any base locale is missing a key (or has an extra one) vs the canonical `en` catalog. i18n.ts lazy-loads each (locale, namespace) chunk via `i18next-resources-to-backend` so Vite code-splits one chunk per locale. -- Locale files are the single source of truth for UI text; no inline substitutions of translated strings. -- The 18 locales are MT-maintained broad coverage with no human owner (r12-wave-a D-005): locale add/remove rides four trigger lines - demand (>=3 non-bot issues for one non-en/zh locale), adoption (>=15% non-English share once telemetry exists), friction (>=2 tray.rs locale reworks/yr or i18n-check fail >=1/mo), quality falsification (RTL/glyph/overflow) - retirement is clean (catalog + gates + tray arms together). -- The bilingual README pair (`README.md` / `README_CN.md`) is itself gated: `scripts/readme-lang-check.cjs` (mounted in `verify-build.sh`) locks the CN mirror to the EN-final section skeleton via an explicit EN<->CN heading mapping (same order, no extras on either side), requires the `GPL-3.0-or-later` License anchor in BOTH files plus working language cross-links, and bans the D-05 drift tokens (mihomo / axum / the unpublished `npm install -g @egressapikey/server`) from both homepages. Editing either homepage means updating both sides and the script mapping table in the same commit. - -### 4. Tests - mandatory per behavior - -- Rust: every public function in `crates/resin-core/` has a unit test in the same file (`#[cfg(test)] mod tests`) or an integration test under `crates/resin-core/tests/`. New behavior without a test is blocked. -- Frontend: Zustand stores and pure reducers have vitest unit tests under `src/**/*.test.ts(x)`. Component interactions have playwright e2e under `e2e/`. -- Evaluator for the whole repo: `bash scripts/verify-build.sh` - runs cargo build, cargo test, pnpm build, pnpm test, the six guard scripts and the mode-a contract gate (ADR-0068 D4); exits non-zero if any fail. CI calls this; so should pre-push hooks. -- Frontend vitest isolation guard (architecture-recovery 18): `node scripts/vitest-isolation-guard.cjs` (also wired into verify-build.sh) fails the build when a view test clicks a collapsible sub-header directly instead of the settled-state helpers (`clickSubHeaderExpand`/`clickSubHeaderCollapse`) - a raw click can land before the default-collapse seeding effect and invert the toggle (full-suite-only flake). - -### 5. CI/CD - multi-platform packaging to release/ - -> **CI-only close-loop (2026-09-19 arbitration of the former P23 hard close-loop - ADR-0072, round11 D-006#1): every source commit MUST yield CI-built delivery evidence.** Local builds are FORBIDDEN (the 2026-09-04 CI-only build policy stands: no local compile/test/packaging). After ANY edit to `src/`, `src-tauri/`, `crates/`, `scripts/`, `tauri.conf.json`, or locale catalogs, delivery evidence = a CI run covering the change - the verify job (verify-build.sh) at minimum, plus a release-matrix dispatch when a testable exe is needed. The chunk-hash freshness check (grep the latest Vite chunk hash inside the staged exe bytes) and the `MainWindowTitle` smoke check apply to the CI-built artifact: `MainWindowTitle` alone proves the shell boots, not that the webview reflects your edits - a stale `dist/` embeds an old frontend and your UI fixes stay invisible (this exact bug bit P22). -> -> **Forbidden shortcut (now a hard pipeline rule)**: `cargo build --release` alone does NOT invoke `beforeBuildCommand` (only `tauri build` does) - it reuses the on-disk `dist/` and ships a stale-bundle binary. Any pipeline producing a deliverable exe MUST run `tsc -b && vite build` before cargo; `scripts/build-all.sh`/`.ps1` embody that order and fail fast with "STALE BUNDLE" when the staged exe lacks the latest chunk hash (T10-audit guard). Under CI-only these scripts are the canonical pipeline reference - read them, do not run them locally. - -- GitHub Actions matrix builds five artifact groups into `release/`: - 1. windows-gui - Tauri MSI + NSIS `-setup.exe` installer (x86_64 msvc) - 2. linux-gui - Tauri `.deb` (Debian) + `.AppImage` (x86_64) - 3. macos-gui - Tauri `.dmg` arm64; x86_64 opt-in via `include_intel_mac` (macos-15-intel, to 2027-08) - 4. gui-portable - one Tauri `--no-bundle` GUI executable per OS - the drop-and-run variant - 5. Backend-only headless target - `cargo build --release -p egressapikey-app --bin egressapikey-headless --features headless` per OS, self-contained tar.gz per platform named `release/-backend/` (headless exe + dist/ + resin sidecar) — see ADR-0043 -- Pipeline reference: `scripts/build-all.sh`/`.ps1` encode the same pipeline order as the CI release jobs (`tsc -b && vite build` -> cargo -> stage, with the chunk-hash guard). Running them locally is FORBIDDEN by the CI-only build policy (ADR-0072); read them to understand the pipeline, do not execute them. -- iOS GUI note: iPadOS cannot run a Tauri shell; the Apple desktop sibling is the macOS `.dmg` (CI + README). No iOS build. -- Artifacts are produced by `tauri-action` (installer + portable) and the backend matrix job; `release/` is gitignored except for tagged release assets uploaded to the GitHub Release. -- Debug-vs-release caveat (verified P2): `cargo build` (debug) emits a binary whose `tauri::generate_context!()` honours `devUrl` (`http://localhost:1420`) when `debug_assertions` is on - the webview tries to load the Vite dev server and shows "ERR_CONNECTION_REFUSED" / "localhost 拒绝连接" if `pnpm dev` is not running. Double-clicking the debug exe with no dev server looks like a GUI that flashes and dies. The **release** binary (`cargo build --release` or `tauri build --no-bundle`) disables `debug_assertions`, so `generate_context!` falls back to `frontendDist: ../dist` and embeds the built assets - it runs standalone with no dev server. The user-facing drop-and-run binary MUST be the release portable (`release/-gui/-portable-gui`), never the debug exe. Verified P2: release exe (7.9MB) boots, `MainWindowTitle = "EgressAPIKEY"`, stays alive 14s+, stdout `tracing initialized`, no stderr/panic. -- **MinGW resource linker fix (windows-gnu target, verified P19)**: auri-winres (build dependency of auri-build) finds the Windows SDK c.exe at C:\Program Files (x86)\Windows Kits\10\bin\...\x64\rc.exe and compiles esource.rc into MSVC .lib format that MinGW gcc.exe CANNOT link - error esource.lib: file not recognized: file format not recognized. Fix: after cargo build fails the link step (the resource.rc is already written), run windres --input /resource.rc --output /resource.lib --output-format coff to overwrite the MSVC .lib with a COFF object, then re-run cargo build --release (the linker will re-link with the fixed object). The previous successful builds produced libresource.a (the windres output) - the SDK rc.exe was likely not in PATH then. This is environmental, not a code bug. - -- **No local release rebuilds (CI-only, ADR-0072)**: the former P23 local two-step guidance is retired - never build release artifacts locally. Release artifacts come from the CI release matrix (`workflow_dispatch`); their freshness is guaranteed by pipeline order (frontend build before cargo) plus the build-all hash guard. -- When handing a build to the user for testing, launch the CI-built portable exe once under `Start-Process` and confirm: `MainWindowHandle != 0`, `MainWindowTitle == "EgressAPIKEY"`, `WorkingSet ~20-40MB`, stdout has `tracing initialized` and no panic line. If `MainWindowTitle` is the exe path or stderr has `ERR_CONNECTION_REFUSED`, it is a debug build mistakenly handed out - re-dispatch the CI release build. - -### 6. Git hygiene - push after every change - -- Commit convention: Conventional Commits `type(scope): summary` (e.g. `docs(r12-wave-p): ...`, `feat(core): ...`) - this matches the entire repo history; the former `: - ` text was retired (audit L-6). -- Every phase/feature commit MUST be pushed (`git push`) so the project is always traceable. Do not accumulate local-only work across phases. -- `.gitattributes` LF policy is authoritative; `git diff --check` must be clean before each commit. Never commit with CRLF outside the allow set (`.bat`, `.ps1`, `.cmd`). -- `git config core.autocrlf false` at repo level (set at init). Do not re-enable autocrlf. -- **Session-artifact hygiene (untrack recipe)**: session-local dirs (`.zcode/`, `.codex-tmp/`, `.omx/`, `context-mode/`, `.scratch/`) must never enter git tracking; each gets a single literal-line `.gitignore` entry (no wildcard broadening). A tracked leftover is untracked with `git rm -r --cached ` — the one permitted index operation outside but's write set; disk files are preserved (never delete). Re-verify with `git status` and `git ls-files `: both must come back empty for the path. -- **Delivery evidence is part of the commit (CI-only, ADR-0072; supersedes the former P23 clause)**: a source commit is finished when CI proves it - the verify job runs verify-build.sh on every push, and a release-matrix dispatch produces the user-facing portable exe (latest Vite chunk hash embedded) when the change needs a testable binary. `target/` and `release/` are gitignored - what you commit is the SOURCE; CI-built artifacts are the deliverable and live outside git. Never hand the user a locally-built exe; do not claim a UI change is testable until its CI-built artifact exists. - -### 7. File integrity (host protocol) - -- New source files: UTF-8 no BOM, LF line endings unless extension is in the CRLF allow set. -- Edits to existing files: re-read the affected region before destructive change; after two failed apply_patch attempts on the same file, do one whole-file rewrite and verify bytes. -- Never inline `$` PowerShell logic; write a `.ps1` and run with `-File` (host transport strips inline `$`). - -### 7.5. Tauri IPC input validation (from P-1 security audit) - -- Every `#[tauri::command]` in `src-tauri/src/commands/` MUST validate its input BEFORE calling resin-core. The kernel is defense-in-depth, not the first line. -- Lane indices (`lane: usize`) must be rejected at the IPC layer with an explicit `Err` when `lane >= resin_core::MAX_LANES`. Any future lane-indexed API that reports success/failure per lane MUST return `bool` (or an equivalent status); callers MUST treat `false` as "no such lane" and never panic. -- Free-form string identifiers (e.g. `authority`) passed to a bounded table or future registry MUST be length-capped (≤253 chars per DNS host) and reject NUL/control characters. The bounded table itself MUST enforce a capacity ceiling (e.g. 256 entries) so a hostile or buggy caller cannot grow memory unbounded. (The TdEwma table this rule was written for was deleted in ADR-0050; the rule stays as the template for any successor.) -- Numeric inputs (`latency_ms`, etc.) that feed an EMA or accumulator MUST be capped to a plausible ceiling before entering the kernel (e.g. `LATENCY_CAP_MS = 24h`) so u64::MAX cannot poison the EMA. -- Do NOT lock the whole `SharedGateway` across an await; commands lock for one short critical section and return. (Current commands are sync; if a future command is async, keep the same invariant.) -- `subscription_add`'s optional `pipeline` parameter (Round 7 ticket 01) accepts exactly `"establish"` or `null`; any other value is rejected at the IPC layer (Rust) and at the TS wrapper before invoking. The parameter only opts the ALREADY-validated subscription import into the backend establish cascade — it never relaxes the name/url/update_interval validation above, and the pipeline queue is bounded (64 entries) with per-entry attempt caps (5) so repeated enqueues cannot grow memory or hammer Resin. -- `subscription_add`'s optional `default_port` parameter (Round 7 ticket 03, D-C1.3) is a plain `u16` validated at both boundaries against `MIN_USER_PORT` (privileged ports rejected; `u16` needs no upper-bound check). Providing it skips the suggest probe in the cascade's optional default-port tail (`resin_core::subscription_pipeline::ensure_default_port`, invoked by the shell right after a GREEN drain pass — the 5-step `run_pipeline`/report shape is untouched). Every conflict (a whitebox row already bound to the platform, the port held by another platform, a foreign Resin listener, a 409 create) is a WARNING + skip, never an overwrite and never an error; unbind goes through the existing `port_remove` path — no new IPC command was added (manifest count unchanged at 78). `port_suggest` now delegates to `resin_core::subscription_pipeline::suggest_free_entry_port` so the GUI and the pipeline share one ADR-0031 suggestion implementation. -- A failed establish cascade compensates only what THAT pass created (Round 7 ticket 04, D-C1.4): `resin_core::subscription_pipeline::compensate_failed_cascade` deletes the Resin-side platform row only when this pass created it AND `apply_strategy`'s failure-owner probe attributes the failure to it; the subscription (user data) and the strategy whitebox (L2) are NEVER touched — whitebox deletion belongs to another path. The ordered sub/plat/port/apply marking persists as schema-locked `last_cascade_error {stage, reason, rollback_actions[]}` on the subscription's status row via `StrategyService::record_cascade_failure` (status subresource — generation does NOT move), cleared by the next Converged write; `validate()` caps the record (8 actions × 256 chars, NUL rejected). No new IPC command (manifest stays 78); the UI surface is read-only projection (snapshot → TS sanitizer → SubscriptionsView hover/expand). -- subscription_remove now runs a best-effort reverse tail (architecture-recovery ticket 05, A-008 / Round 9 D-007 option B): after the Resin DELETE succeeds, `resin_core::subscription_pipeline::remove_default_port_if_orphaned` releases every whitebox entry-port row bound to the deleted subscription's platform through the same three-layer order as `port_remove` (L2 whitebox retain→apply first, then the L3 Resin endpoint delete — Mode B only; Mode A's shell listener comes down with the whitebox apply and Resin is never contacted). The read-only id="default" endpoint is never a deletion candidate; a failed release is a `tracing::warn!` (the leftover surfaces as drift), never an IPC error — the removal itself succeeded. The (e)→(f) create-then-write window in `ensure_default_port` is likewise closed: a whitebox-write failure compensates by DELETE-ing exactly the endpoint THIS step created (fresh live read; idempotent no-op when already gone), and step (a)'s same-platform binding check already provides the idempotence the ticket demands. No new IPC command (manifest stays 79). - -### 7.6. Sidecar SSRF guard + IPC surface discipline (Re8 audit) - -- mihomo status (ADR-0050): the `crates/resin-core/src/mihomo.rs` module (MihomoController + its loopback SSRF guard, MihomoConfig, subscription compile helpers) was DELETED with the rest of the dead kernel face (zero external references; it was never instantiated). If mihomo REST control is ever reintroduced, the controller MUST re-assert the Re8 rule before any IPC wiring: `api_base` validated as loopback-only (`http://127.0.0.1` / `http://localhost` / `http://[::1]`, plus https variants) at construction, with reject + accept-variant unit tests, and no frontend-controlled `String` may ever construct it (config stays server-side trust). -- Headless control surface (architecture-recovery round 8, ticket 03 / A-007): the - `egressapikey-headless` router MUST keep every route - including the static - fallback - behind the `security_guard` layer in `src-tauri/src/headless_main.rs`, - which enforces a shared `--auth-token` on `/api/v1/*` + `/metrics/*` and a - `Host` / `Origin` allowlist on every request. The token is CSPRNG-generated - (`getrandom`); time + PID entropy is forbidden. Never register a route above the - `.layer(...)` call, and never add a route that bypasses the guard. Primitives + - unit tests: `src-tauri/src/headless_security.rs`. Operator threat model: - `docs/how-to/HEADLESS_DEPLOYMENT.md`. -- Frontend `invoke()` surface (authoritative manifest, architecture-recovery ticket 03): the command list lives in `docs/agents/ipc-manifest.md` (the `ipc-manifest` fence below is a pointer only). It is REGENERATED by `node scripts/ipc-manifest-check.cjs --write` and machine-checked on every build (`pnpm ipc:check`, `scripts/verify-build.sh`, CI); the check fails on command add/remove/rename, `generate_handler!` registry drift, manifest drift, or stale file attribution. Do not hand-edit: run the regenerator. Each TS wrapper in `src/lib/ipc.ts` validates input at the TS boundary (assertShortName/assertAuthority/assertIp, length caps, lane range `0..MAX_LANES=50`, latency cap, URL `http(s)://` prefix) BEFORE invoking, and treats the Rust response (`reason`, `lane`, `account`) as untrusted — never piped into another URL or command. The Rust side re-validates the same bounds in `commands/` domain modules. When wiring any new IPC command, the TS wrapper MUST follow this same validate-then-invoke contract. - -**Echo command list (kept for IPC contract compatibility; the shell does not implement their semantics — ADR-0050 deleted the kernel face)**: - -- `account_add` (src-tauri/src/commands/platform.rs:64): validates inputs (incl. the lane range) then returns `Ok(())` — Resin owns account semantics since ADR-0050. Emits `tracing::warn!(target: "ipc.account_deprecated")` per call. -- `account_bind_ip` (src-tauri/src/commands/platform.rs:85): validates inputs then returns `Ok(true)` — same echo semantics. Emits the same deprecation warn. TS wrapper `ipcAccountBindIp` carries the matching `@deprecated` + dev-only `console.warn`. - -Removal condition: drop these from the manifest + registry only when Resin's account REST surface changes shape (T16 owns the account header-rules integration; do not delete in a docs round). - -```ipc-manifest -# Moved to docs/agents/ipc-manifest.md - regenerate with -# node scripts/ipc-manifest-check.cjs --write. Commands: 87 -``` - -- If mihomo REST control is ever reintroduced (ADR-0050 is the authoritative record of the `mihomo.rs` deletion): never expose `MihomoController`, `CoreConfig.mihomo_api`, or `CoreConfig.mihomo_secret` through a `#[tauri::command]` that takes a raw `String` and constructs the controller from it; `api_base` must pass the loopback-only validation described above (non-loopback targets are rejected); and config must come from `tauri-plugin-store` settings.json (server-side trust), not from the webview. -- If `MihomoController` is ever reintroduced, it must not be wired through a frontend-controlled constructor without revisiting this section; keeping it uninstantiated by the Tauri shell was the pre-deletion safety boundary and remains the default stance until such a reintroduction is explicitly designed. - -### 8. Subagent policy for this repo - -- This thread runs with subagents DISABLED (per user instruction). Do NOT spawn Codex native subagents or OMX team/worker lanes. Execute everything single-threaded in this agent. - -### 9. Don't revert work you didn't make - -- If uncommitted changes appear that this agent did not make, treat them as user/external and do not revert. Either ignore (unrelated) or build with them (affects the task). - -### 10. Update this file in the same commit that changes the project - -- AGENTS.md is the source of truth. When structure, conventions, module boundaries, tech stack, or release/push protocol changes, update this file in the SAME commit that introduces the change. - -### 11. License layering & third-party provenance (ADR-0067) - -- The repo declares GPL-3.0-or-later via the root `LICENSE` (official verbatim GNU text — never reword it) and the three fields `README.md` License section / root `Cargo.toml` `[workspace.package].license` / `package.json` `license`, kept equal by `scripts/license-field-check.cjs` (mounting into verify-build belongs to the CI-gate ticket; run `node scripts/license-field-check.cjs` before committing license-touching changes). Member crates inherit via `license.workspace = true`, never hardcode. -- `THIRD_PARTY.md` is the single registry for third-party components and records the two-layer value per component: declared license vs dependency-tree truth. Resin v1.2.0 = declared MIT + dependency-tree GPL-3.0-or-later via sing-box (`resin/go.mod` pins `github.com/sagernet/sing-box v1.12.21`; sing-box's upstream LICENSE is GPLv3-or-later). -- Mere-aggregation invariant (ADR-0067 D3): shell ↔ sidecar interact only over the loopback REST seam (`ResinClient`). No FFI, no in-process linking, no source embedding in either direction — breaking this voids the license layering and requires revisiting ADR-0067. -- On any `docs/RESIN_UPSTREAM_MANIFEST.yaml` version bump: re-verify the license facts against upstream originals at the exact new tag (go.mod + LICENSE URL quotes, never memory) and update `THIRD_PARTY.md` + the manifest two-layer `license` field in the same commit (ADR-0017 amendment). - -## Agent skills - -### Issue tracker - -Issues are tracked as local markdown under `.scratch/` (one directory per feature: -spec, tickets, handoffs, reports); remote `github.com/Xxx91n/EgressAPIKEY` is -available with a declared switch path to GitHub Issues. See `docs/agents/issue-tracker.md`. - -### Triage labels - -Default five-label vocabulary (`needs-triage` / `needs-info` / `ready-for-agent` / -`ready-for-human` / `wontfix`), recorded as the `Status:` line of each issue file. See `docs/agents/triage-labels.md`. - -### Domain docs - -Single-context layout: root `CONTEXT.md` glossary + `docs/adr/`. See `docs/agents/domain.md`. - -### Community & contribution files - -Moved to [docs/agents/community-files.md](docs/agents/community-files.md) (CONTRIBUTING / ISSUE_TEMPLATE / PR_TEMPLATE / SECURITY / CODE_OF_CONDUCT inventory). - -### Architecture & Phase History (externalized) - -> The following sections were moved to `docs/agents/` to keep this file under the 32 KiB `project_doc_max_bytes` limit. -> Content is unchanged (extraction only, progressive disclosure per writing-for-agents). - -**Architecture implementation** (AGENTS.md former §11-17): [docs/agents/architecture-state.md](docs/agents/architecture-state.md) - -- Runtime wiring state, Resin sidecar lifecycle (G1), Ghost safety net (G3), ResinClient (G2) -- IPC retarget to Resin sidecar, Resin → shell signal channel (G4 = IPC retarget + sidecar-status event subscription, no HTTP webhook), CI/CD release pipeline (G5) - -**Phase history** (AGENTS.md former §18-66, P9-P26/T7-T22/R1-R2/C1-C2): [docs/agents/phase-history.md](docs/agents/phase-history.md) - -- These are **process artifacts** (point-in-time phase completion records), not current truth. -- For current architecture overview: [docs/architecture/](docs/architecture/) -- For architectural decisions: [docs/adr/](docs/adr/) -- For what shipped (milestones): [CHANGELOG.md](CHANGELOG.md) +- Issue tracker: local markdown under `.scratch/` — declaration in `docs/agents/issue-tracker.md` +- Domain docs: `CONTEXT.md` (glossary) · `docs/adr/` · `docs/architecture/` · `docs/agents/` +- Agent operating rules (tool routing, CodeGraph, file integrity): `docs/agents/README.md` +- Review conventions (i18n / tests / CI-only / license / IPC): `CONTRIBUTING.md` +- VCS discipline: GitButler `but` for all VC writes — never `git` mutations directly; **never push or open PRs unless the user asks**; do not rewrite pushed/shared history; never revert changes this agent did not make diff --git a/CONTEXT.md b/CONTEXT.md index 2f78f7cf..e3f1d1f6 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -580,7 +580,7 @@ with a subscription bound draws unlabeled unconditional edges (see Strategy-Labeled Edge). Rejected the older "subscription group = a bundle of regions" reading, where dragging a subscription silently expanded into a hand-written region list: it desynchronized the recorded intent from the -applied projection (ADR-0075). +applied projection (former ADR-0075 — record retired 2026-10-15). _Avoid_: region bundle, subscription expansion, subscription group drag-fill ### Strategy-Labeled Edge diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a14a78c8..dde54ad6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,18 +47,21 @@ CI runs the same script on every push and pull request. Focused checks: ## Conventions that gate review -These are enforced by reviewers and CI; full operating rules live in [AGENTS.md](AGENTS.md): +These rules are authoritative here and are enforced by reviewers and CI (`scripts/verify-build.sh` mounts the machine checks): -- **i18n full-key coverage** — every user-visible string comes from `src/locales//*.json` via `t()` / `Trans`; a new key must land in all 18 base locales in the same commit. Never hard-code locale text in components. -- **Test per behavior** — every public function in `crates/resin-core/` carries a unit test (same file or `crates/resin-core/tests/`); Zustand stores and pure reducers get vitest tests under `src/`; component interactions get Playwright e2e coverage under `e2e/`. -- **License discipline** — the repository is GPL-3.0-or-later ([LICENSE](LICENSE)); the three license fields are machine-checked; vendored third-party components are registered in [THIRD_PARTY.md](THIRD_PARTY.md). -- **Docs routing** — the README is a homepage; deep content belongs in `docs/` (architecture, ADRs, how-tos). +- **i18n full-key coverage** — every user-visible string in `src/` comes from `src/locales//*.json` via `t()` / `Trans`; never hard-code locale text in components. A new key must land in **all 18 base locales** in the same commit (`pnpm i18n:check` fails the build otherwise); the `Locale` union in `src/store/appStore.ts` and the `ALL` list in `scripts/i18n-check.cjs` stay in lockstep with the `src/locales/` directories. +- **Test per behavior** — every public function in `crates/resin-core/` carries a unit test (same-file `#[cfg(test)]` or `crates/resin-core/tests/`); Zustand stores and pure reducers get vitest tests under `src/`; component interactions get Playwright e2e coverage under `e2e/`. The evaluator for the whole repo is `bash scripts/verify-build.sh` (what CI runs on every push/PR). +- **CI-only delivery** (ADR-0072) — local builds are forbidden as delivery evidence: a source commit is finished when the CI verify job proves it, and a testable exe comes from a release-matrix dispatch. Never hand out a locally-built binary; never claim a UI change is testable before its CI-built artifact exists. +- **Bilingual README pair** — `README.md` is the EN-canonical homepage skeleton (preserve its section order; route deep content to `docs/`); `README_CN.md` is the structure-locked CN mirror held 1:1 by `scripts/readme-lang-check.cjs` — update both sides and the script mapping table in the same commit. The `## License` body stays exactly `GPL-3.0-or-later`. +- **License discipline** — the repository is GPL-3.0-or-later via [LICENSE](LICENSE) (verbatim GNU text — never reword); the README / workspace `Cargo.toml` / `package.json` license fields are machine-checked (`scripts/license-field-check.cjs`); vendored third-party components register in [THIRD_PARTY.md](THIRD_PARTY.md) with the two-layer declared-vs-tree value. Mere-aggregation invariant (ADR-0067): shell ↔ sidecar interact only over the loopback REST seam — no FFI, no in-process linking, no source embedding. On any `docs/RESIN_UPSTREAM_MANIFEST.yaml` version bump, re-verify license facts against upstream originals at that tag and update `THIRD_PARTY.md` + the manifest in the same commit. +- **IPC & security contracts** — every `#[tauri::command]` validates input at the IPC layer before calling resin-core (lane range `0..MAX_LANES`, length caps ≤253 + NUL rejection, latency cap, `http(s)://` URL prefix); TS wrappers in `src/lib/ipc.ts` validate-then-invoke and treat Rust responses as untrusted. The headless router keeps **every** route behind `security_guard` in `src-tauri/src/headless_main.rs` (`--auth-token` on `/api/v1/*` + `/metrics/*`, Host/Origin allowlist, CSPRNG token — never register a route above the `.layer(...)` call). The IPC manifest (`docs/agents/ipc-manifest.md`) is machine-checked — regenerate with `node scripts/ipc-manifest-check.cjs --write`, do not hand-edit. +- **Docs routing** — the README is a homepage; deep content belongs in `docs/` (architecture, ADRs, how-tos). Retired docs are deleted, not archived in-tree — history lives in git. ## Commit and branch style - Branch names are free-form; keep one logical change per branch and pull request. - Commit subjects stay short and area-scoped (for example, `gateway: lane hash + SSE lease`); the body explains what changed and why. -- Update [AGENTS.md](AGENTS.md) in the same commit when a change alters structure, conventions, or the release protocol. +- Update this file (conventions) and the pointer docs (`AGENTS.md`, `docs/agents/`) in the same commit when a change alters structure, conventions, or the release protocol. ## Pull requests diff --git a/docs/DOCUMENT_GOVERNANCE_PLAN.md b/docs/DOCUMENT_GOVERNANCE_PLAN.md deleted file mode 100644 index dc2b5591..00000000 --- a/docs/DOCUMENT_GOVERNANCE_PLAN.md +++ /dev/null @@ -1,368 +0,0 @@ ---- -date: 2026-08-19 | branch: doc-fix | status: PLAN v2 (boxing exp merged, pending user) -explore: codegraph (86 files 1452 nodes) + grill 18+35 skill + industry 32 sources + neat-freak + writing-for-agents -ref: D:/Aworker/crx/boxing completed 2 commits (3-layer split + CI governance) ---- - -# Document Governance Plan: grill Workflow Blind Spot Fix - -## 1. Root Cause - -The grill suite (ask-matt index) main flow designed clear doc output paths: - -### grill 18 skill complete workflow suite (ask-matt confirmed) - -Main flow (idea -> ship): - -1. /grill-with-docs: interview -> CONTEXT.md (root) + docs/adr/ -2. /to-spec: spec.md -> .scratch// (gitignored) or issue tracker -3. /to-tickets: NN-slug.md -> .scratch//issues/ (gitignored) -4. /implement: from ticket, fresh context - -On-ramps: - -- /triage: raw issues -> agent-ready issues -> main flow at /implement -- /diagnosing-bugs: hard bugs -> regression test -> /improve-codebase-architecture -- /wayfinder: huge foggy -> decision tickets -> /to-spec -> main flow - -Standalone: - -- /grill-me: stateless interview -- /grilling: interview primitive -- /resolving-merge-conflicts: merge/rebase hunk by hunk -- /research: background agent -> cited markdown -- /to-questionnaire: questions for someone else - -Vocabulary underneath: - -- /domain-modeling: domain language -> CONTEXT.md + ADR -- /codebase-design: module shape vocabulary (depth/seam/adapter) - -setup: - -- /setup-matt-pocock-skills: docs/agents/{issue-tracker,domain,triage-labels}.md - -### doc output contract - -| grill output | active location | archive location | -|---|---|---| -| CONTEXT.md | root | -- | -| ADR | docs/adr/ | -- | -| spec.md | .scratch/ | post-commit -> docs/history/specs/ | -| tickets | .scratch/ | done -> docs/history/tickets/ | -| handoff | (temp) | docs/history/handoffs/ | -| research | docs/research/ | -- | -| docs/agents/*.md | docs/agents/ | -- | - -### what actually happened - -Three categories of output got dumped into docs/ instead: - -1. GRILL_T* execution plans (22 files ~3000 lines) - session-temp or done phases -2. HANDOFF_* handoff docs (10 files ~1200 lines) - session artifacts -3. User prompt archive (USER_PROMPT_ARCHIVE + T9_MASTER_GOAL_PROMPT) - not engineering docs - -docs/ now: 9364 lines / ~95 files. 49 ADRs are compliant. 63 root-level md files (non-adr). -No .archive/, no .scratch/, no docs/agents/, no docs/history/, no CHANGELOG.md. - -### Codex hard limit (from boxing project research) - -- project_doc_max_bytes = 32 KiB (Codex default, configurable) -- Current AGENTS.md: 230637 bytes = 225 KiB, 7x over limit -- Over-limit: Codex silently truncates deepest file; AGENTS.md sections 10-29 likely never read by agent -- boxing project (286 lines/39218 bytes) identified this; we are 7x worse -- Industry consensus (atomcode): 60-300 line sweet spot, 150-200 start splitting -- What to keep: copy-pasteable exact commands, rules diverging from language defaults, Always/Ask first/Never boundaries -- What to split out: not-every-task needs it -> separate file + pointer - -## 2. Industry Four-Layer Separation Model (32 sources) - -| Layer | Framework | Manages | Location | -|---|---|---|---| -| Content organization | Diataxis | text classification | docs/ by quadrant | -| Architecture description | C4 model | diagram zoom | docs/architecture/ | -| Decision records | ADR/MADR | why archived | docs/adr/NNNN-*.md | -| Agent instructions | AGENTS.md | how agent works | root AGENTS.md <500 lines | -| Milestone timeline | Keep a Changelog | what shipped | CHANGELOG.md | - -Key finding (arXiv 2026 ETH paper): agent instruction files should only contain commands and pointers, not architecture descriptions; inline overviews increase cost +20% without benefit. - -### neat-freak "reduce before add" principle - -- One-time plan docs (22 GRILL_T*): after merging effective content into formal docs, files are delete candidates -- Don't add new docs before evaluating if existing ones can be trimmed or deleted - -### writing-for-agents information hierarchy (3 levels) - -1. In-file step: what agent does every run (keep in AGENTS.md) -2. In-file reference: rules agent consults on demand (can keep in AGENTS.md) -3. Disclosed reference: only some branches need it -> separate file + pointer (phase sections) - -Branching test: inline what every branch needs, pointer what only some branches reach. -Our phase sections (10-29) are only needed when reviewing project history -> should be disclosed. - -### boxing project confirmation - -Three-layer separation is the user's own governance decision, NOT a grill skill built-in standard. -Grill has partial answers (CONTEXT.md / ADR / docs/agents/), but doesn't cover -"Critical Lessons Learned" and "Performance Anti-Patterns" engineering convention placement. - -## 3. Migration Plan (git mv, preserves history) - -### Unchanged - -- docs/adr/ (49 files, MADR style, compliant) -- .github/ (workflows, ISSUE_TEMPLATE) -- AGENTS.md (root, needs slimming but position unchanged) - -### Moves - -| Current | Target | Count | Reason | -|---|---|---|---| -| docs/GRILL_T*.md | docs/history/phases/ | 22 | done phase plans, archive | -| docs/HANDOFF_*.md | docs/history/handoffs/ | 10 | session artifacts | -| docs/USER_PROMPT_ARCHIVE.md | docs/history/prompts/ | 1 | not engineering doc | -| docs/T9_MASTER_GOAL_PROMPT.md | docs/history/prompts/ | 1 | same | -| docs/T4_*.md, T7-*.md | docs/history/phases/ | 3 | done | -| docs/ARCHITECTURE.md | docs/architecture/ | 1 | Diataxis explanation | -| docs/CONTEXT.md | root CONTEXT.md | 1 | grill design position | -| docs/glossary.md | docs/reference/ | 1 | Diataxis reference | -| docs/RESIN_ROUTING_*.md | docs/research/ | 1 | research | -| docs/KEY_ENDPOINT_*.md | docs/research/ | 2 | research | -| docs/PROTOCOL_WEIGHT_*.md | docs/research/ | 1 | research | -| docs/MEMORY_REUSE_DECISION.md | docs/architecture/ | 1 | arch decision | -| docs/REFACTOR_PLAN*.md | docs/architecture/ | 3 | arch refactor plan | -| docs/HEADLESS_*.md | docs/how-to/ | 2 | ops guide | -| docs/RELEASE.md | docs/how-to/ | 1 | release guide | -| docs/PONYTAIL_DEBT*.md | root PONYTAIL_DEBT_LEDGER.md | 2 | active debt ledger | -| docs/PROJECT_PLAN.md | -> CHANGELOG.md | 1 | milestones to changelog | -| docs/GRILL_ISSUES_BACKLOG.md | docs/history/phases/ | 1 | done backlog | - -### New files - -- CHANGELOG.md (Keep a Changelog 2.0 template, six types + [Unreleased]) -- docs/architecture/README.md (entry point, links to ARCHITECTURE.md + MEMORY_REUSE_DECISION.md) -- docs/history/README.md ("HISTORICAL ARCHIVE - not current truth", reader x purpose) -- docs/agents/README.md (reader x purpose per writing-for-agents) -- .markdownlint-cli2.jsonc (lint config) -- .github/workflows/docs-lint.yml (markdownlint + lychee + size warning) -- .gitignore add .scratch/ - -## 4a. AGENTS.md Slimming (most critical fix) - -Current AGENTS.md: 852 lines / 230637 bytes (7x over 32 KiB Codex limit) - -### Slimming strategy (writing-for-agents progressive disclosure) - -Keep (agent uses every run): - -- context-mode routing block (ctx_* tool routing) -- CodeGraph conventions -- i18n conventions (full key coverage, 18 locale) -- Test conventions (cargo test, vitest, e2e) -- CI/CD conventions (build-all, release staging, chunk hash verify) -- Git hygiene (push after every commit, LF policy) -- Security redlines (IPC validation, SSRF guard, sidecar SSRF) -- Pointers to docs/ ("architecture see docs/architecture/, decisions see docs/adr/") - -Migrate to docs/architecture/ (reference level, some branches need): - -- Runtime state (current section 11) -> docs/architecture/runtime-state.md -- Storage locations -> docs/architecture/storage.md -- Sidecar lifecycle -> docs/architecture/sidecar.md -- Ghost safety net -> docs/architecture/ghost-safety.md - -Migrate to CHANGELOG.md (process artifacts, milestones): - -- IPC retarget (sections 15-20) -- Resin → shell signal channel (section 16; G4 = IPC retarget + sidecar-status event subscription, no HTTP webhook) -- CI/CD release pipeline (section 17) -- P9-P24 phases (sections 18-29) -- R1-R2 refactor (section 23) - -Expected: AGENTS.md from 230637 bytes -> ~30000 bytes (under 32 KiB) - -## 5. Risks and Mitigation - -### Risks - -1. Other parallel fix branches may reference docs/GRILL_T\* / docs/HANDOFF_\* paths - - Mitigation: git mv preserves history, all paths are rename not delete - - Mitigation: check other branches for references first (grep) - -2. AGENTS.md slimming may lose current truth - - Mitigation: phase sections migrate to CHANGELOG.md, not deleted; AGENTS.md keeps pointers - - Mitigation: AGENTS.md top block (context-mode, CodeGraph, i18n, test, CI/CD) stays, these are agent commands not milestones - -3. docs/CONTEXT.md -> root CONTEXT.md may conflict with existing file - - Mitigation: root already has CONTEXT.md with full glossary (verified) - - Mitigation: merge if needed, don't overwrite - -4. docs/history/ docs may be misread by LLM as current truth - - Mitigation: docs/history/README.md marks "HISTORICAL ARCHIVE - not current truth" - - Mitigation: same as ADR superseded status, kept but not trusted - -### Untouched - -- Source code (no .rs/.ts/.tsx changes) -- ADR content (49 ADRs unchanged, position confirmed) -- docs/adr/ structure (unchanged) -- Tests (no cargo/pnpm runs, pure doc migration) -- Build artifacts (no exe involved) - -### Additional risks (from boxing project) - -- AGENTS.md slimming is neat-freak "report before deciding": reported, awaiting user confirm -- Three-layer separation is user governance decision, not grill skill built-in standard -- README reader x purpose: writing-for-agents requires pointer sections mark trigger conditions -- Completed grill plan archival: neat-freak sync-matrix says "after merging effective content, file is delete candidate" -- Parallel fix branches: other branches may change files, check worktree before executing - -## 6. Execution Order - -Phase 1: Create dirs + .gitignore (low risk) - -- mkdir docs/history/phases docs/history/handoffs docs/history/prompts -- mkdir docs/architecture docs/reference docs/research docs/how-to -- mkdir docs/agents -- .gitignore add .scratch/ - -Phase 2: Archive mixed products (git mv, preserve history) - -- git mv docs/GRILL_T*.md docs/history/phases/ -- git mv docs/HANDOFF_*.md docs/history/handoffs/ -- git mv docs/USER_PROMPT_ARCHIVE.md docs/T9_MASTER_GOAL_PROMPT.md docs/history/prompts/ -- git mv docs/T4_*.md docs/T7-*.md docs/history/phases/ -- Create docs/history/README.md (HISTORICAL ARCHIVE marker) - -Phase 3: Organize active docs (git mv) - -- git mv docs/ARCHITECTURE.md docs/architecture/ -- git mv docs/CONTEXT.md root (merge if existing) -- git mv docs/glossary.md docs/reference/ -- git mv docs/RESIN_ROUTING_*.md docs/KEY_ENDPOINT_*.md docs/PROTOCOL_WEIGHT_*.md docs/research/ -- git mv docs/MEMORY_REUSE_DECISION.md docs/REFACTOR_PLAN*.md docs/architecture/ -- git mv docs/HEADLESS_*.md docs/RELEASE.md docs/how-to/ -- git mv docs/PONYTAIL_DEBT*.md root -- git mv docs/PROJECT_PLAN.md docs/GRILL_ISSUES_BACKLOG.md docs/history/phases/ - -Phase 4: Create CHANGELOG.md - -- Extract completed phases from git log -- Use Keep a Changelog 2.0 template -- Fill [Unreleased] + released versions -- Merge AGENTS.md phase sections (10-29) effective content into CHANGELOG - -Phase 5: Slim AGENTS.md (highest risk, must be after Phase 4) - -Per writing-for-agents branching test: - -Keep (agent uses every run): - -- context-mode routing block (ctx_* tool routing) -- CodeGraph conventions -- i18n conventions (full key coverage, 18 locale) -- Test conventions (cargo test, vitest, e2e) -- CI/CD conventions (build-all, release staging, chunk hash verify) -- Git hygiene (push after every commit, LF policy) -- Security redlines (IPC validation, SSRF guard, sidecar SSRF) -- Pointers to docs/ ("architecture see docs/architecture/, decisions see docs/adr/") - -Migrate to docs/architecture/ (reference level): - -- Runtime state (section 11) -> docs/architecture/runtime-state.md -- Storage locations -> docs/architecture/storage.md -- Sidecar lifecycle -> docs/architecture/sidecar.md -- Ghost safety net -> docs/architecture/ghost-safety.md - -Migrate to CHANGELOG.md (process artifacts): - -- IPC retarget (sections 15-20) -- Resin → shell signal channel (section 16; G4 = IPC retarget + sidecar-status event subscription, no HTTP webhook) -- CI/CD release pipeline (section 17) -- P9-P24 phases (sections 18-29) -- R1-R2 refactor (section 23) - -Not called (don't need): - -- grill-with-docs: this task reorganizes existing, not new domain docs -- codebase-design: this task doesn't touch code module architecture - -Phase 6: CI governance (low risk) - -- .markdownlint-cli2.jsonc -- .github/workflows/docs-lint.yml -- Reference boxing project ai-docs-governance.yml (dead-link + layer-separation + size-warning) - -## 7. Off-the-Shelf Template Wheels - -| Need | Template | Repo | Usage | -|---|---|---|---| -| ADR template | adr/madr | GitHub 2.4k stars | current, keep | -| Changelog manual | Keep a Changelog 2.0 | keepachangelog.com | copy header + six types | -| Changelog auto | orhun/git-cliff | 12.1k stars | conventional commits | -| Markdown lint | DavidAnson/markdownlint-cli2-action | 188 stars | CI job | -| Link check | lycheeverse/lychee-action | 508 stars | CI + cron | -| Prose lint | vale-cli/vale | 6k stars | error-level gate | -| AGENTS.md template | Taiizor/agents-md-cookbook | GitHub | stack template + lint CI | - -## 8. Sources - -### Industry research (32 sources, atomcode 2 rounds) - -Diataxis + C4 + ADR/MADR + AGENTS.md (agents.md official, 60k+ repos, LF AAIF) - -- markdownlint/lychee/Vale (GitHub Action level) -- arXiv 2602.11988 (ETH: agent instructions should only have commands + pointers) -- Tauri/React/Rust/Vite/Airflow repo evidence -- Codex hard limit project_doc_max_bytes = 32 KiB (from boxing project research) - -### grill workflow suite (18 engineering + 35 full skills) - -main flow: grill-with-docs/to-spec/to-tickets/implement -on-ramps: triage/diagnosing-bugs/wayfinder -standalone: grill-me/grilling/research/handoff/to-questionnaire -vocabulary: domain-modeling/codebase-design -setup: setup-matt-pocock-skills -> docs/agents/ - -### neat-freak + writing-for-agents (boxing project already invoked) - -neat-freak: knowledge governance closeout, "reduce before add", sync-matrix (one-time plan docs delete candidates) -writing-for-agents: information hierarchy 3 levels (step/reference/disclosed), branching test, context pointer - -### boxing project reference (completed 2 commits) - -boxing (D:/Aworker/crx/boxing): 286 lines/39218 bytes AGENTS.md -> 257 lines/~27000 bytes -Completed: 3-layer split (git mv 4 files + pointer + governance section) + CI governance (ai-docs-governance.yml) -Our project is 7x more severe (230637 bytes), but approach is directly referenced - -### Project exploration (codegraph) - -86 files 1452 nodes 4493 edges; 3-layer arch (resin-core -> src-tauri -> src); -IPC all forward to Resin sidecar; 7 Views (topology/platforms/nodes/settings/processRoute/subscriptions/diagnostics) - -## 9. Decision Points (need user confirmation) - -### Decision 1: AGENTS.md slimming scope - -Current sections 10-29 are milestones. Per arXiv paper and neat-freak, should migrate to CHANGELOG. -But some sections contain still-effective engineering conventions (e.g. section 7.5 IPC validation, 7.6 SSRF guard). - -Question: migrate sections 10-29 wholesale to CHANGELOG (pure history), or split per-section (conventions stay in AGENTS.md, milestones migrate)? - -Recommendation (boxing experience): split per-section. Conventions are live Never-boundaries, milestones are process artifacts. - -### Decision 2: Archive location naming - -boxing uses docs/history/ (inside docs/), I initially proposed .archive/ (at repo root). - -Question: use docs/history/ (boxing-validated, CI-compatible) or .archive/ (repo root)? - -Recommendation: use docs/history/ like boxing, compatible with CI layer-separation check. - -### Decision 3: CI governance depth - -boxing landed ai-docs-governance.yml (dead-link + layer-separation + size-warning). -We could copy that or add markdownlint + lychee as independent jobs. - -Recommendation: copy boxing ai-docs-governance.yml structure + add markdownlint-cli2-action. diff --git a/docs/PONYTAIL_DEBT_LEDGER.md b/docs/PONYTAIL_DEBT_LEDGER.md index 7fd0c8ba..2f0b4c59 100644 --- a/docs/PONYTAIL_DEBT_LEDGER.md +++ b/docs/PONYTAIL_DEBT_LEDGER.md @@ -1,6 +1,6 @@ # Ponytail Debt Ledger -Re-verified: 2026-08-17 (post-T17 canvas v4 audit + ADR-0041 S1 spec-gap fix). Source-tagged markers are the authoritative scan target: `rg -n "ponytail:" --glob "!*.md" --glob "!docs/**"` returns 6 hits (was 2 on 2026-08-13). Three of the 6 were present-but-unlisted since T15 / T16 / T8-era diffs; the 4 new hits across that span were not synced into this ledger, which is the drift fixed here. The 2026-08-13 entry below preserved verbatim as history, then a 2026-08-17 section supersedes the count. +Re-verified: 2026-08-17 (post-T17 canvas v4 audit + ADR-0041 (retired 2026-10-15) S1 spec-gap fix). Source-tagged markers are the authoritative scan target: `rg -n "ponytail:" --glob "!*.md" --glob "!docs/**"` returns 6 hits (was 2 on 2026-08-13). Three of the 6 were present-but-unlisted since T15 / T16 / T8-era diffs; the 4 new hits across that span were not synced into this ledger, which is the drift fixed here. The 2026-08-13 entry below preserved verbatim as history, then a 2026-08-17 section supersedes the count. Re-verified: 2026-08-13 (post-T7 diagnostics page refactor + clear_os_proxy audit fix). Source-tagged markers are the authoritative scan target: `git grep -rnE '(#|//) ?ponytail:' -- "*.rs" "*.ts" "*.tsx" "*.json"` returns 2 hits (unchanged from 2026-08-12). @@ -13,7 +13,7 @@ Re-verified: 2026-08-13 (post-T7 diagnostics page refactor + clear_os_proxy audi | `src-tauri/src/sidecar.rs` L882 | `// - ponytail: do NOT add tauri-plugin-notification just for this — the webview already renders a banner on events.` | keep | The ghost safety net (`spawn_health_poll`) reports unhealthy sidecar via a `sidecar-status` event + OS system-proxy clear. A native notification crate would be a redundant surface; the React-shell banner is the single notification channel. No new native crate. | | `src-tauri/src/commands/mod.rs` L908 | `let mut guard = sidecar.child.lock().unwrap_or_else(\|e\| e.into_inner()); // ponytail: poison-safe, matches AGENTS §7.5 no-panic-in-production` | keep | `Mutex::lock().unwrap()` would panic the IPC command thread if a prior holder panicked; `unwrap_or_else(e => e.into_inner())` recovers the poisoned lock and continues shutdown. Matches AGENTS §7.5 `LeaseTable::evict_lane` pattern — no panic in production paths. | | `src/lib/ipc.ts` L669 | `// ponytail: DONE (P31-T6, commit 900ddc5) — extractIpcErr now used by translateError in all GUI catch blocks` | done | The marker is a closed-loop receipt: `extractIpcErr` narrows every `catch (e)` in `src/views/*.tsx` via `translateError(e, t)` (P31-T6 unified the GUI error path). No remaining `catch` block stringifies `e` directly. Line number updated L646 → L669 (T15 strategyConfig JSON editor edit shifted the file). | -| `src/views/TopologyView.tsx`L234 |`// ponytail: S2 helper dedupNodesByHash (defined near the top of this file) is only called from the subscription route above + its unit test, not from this region route.`| keep | ADR-0041 S2 contract says both routes must dedup by`node_hash`. The subscription route calls the shared `dedupNodesByHash`helper; the region route dedups inline via`seenGlobal`Set *while* simultaneously building`regionMap` (total/healthy/subs/nodeRows). Fusing those two concerns into the dedup-only helper would couple aggregation into a single-purpose function and grow the regression surface. Track here; promote to the helper only when aggregation-vs-dedup can be cleanly separated. S2 contract honored, different shape. | +| `src/views/TopologyView.tsx`L234 |`// ponytail: S2 helper dedupNodesByHash (defined near the top of this file) is only called from the subscription route above + its unit test, not from this region route.`| keep | former ADR-0041 (retired 2026-10-15) S2 contract says both routes must dedup by`node_hash`. The subscription route calls the shared `dedupNodesByHash`helper; the region route dedups inline via`seenGlobal`Set *while* simultaneously building`regionMap` (total/healthy/subs/nodeRows). Fusing those two concerns into the dedup-only helper would couple aggregation into a single-purpose function and grow the regression surface. Track here; promote to the helper only when aggregation-vs-dedup can be cleanly separated. S2 contract honored, different shape. | ## Status legend @@ -46,7 +46,7 @@ Re-verified: 2026-08-13 (post-T7 diagnostics page refactor + clear_os_proxy audi rg -n "ponytail:" --glob "!*.md" --glob "!docs/**" ``` -6 source hits on 2026-08-17 (was 2 on 2026-08-13). Five keep, one done. No stale markers remain. Drift surfaced in the post-T17 audit: the T15 + T16 + T17 commits added 4 keep markers (`commands/mod.rs` L908 poison-safe mutex, `sidecar.rs` L330 Job Object race, `sidecar.rs` L882 notification crate, `TopologyView.tsx` L763 S2 helper deferral) without a corresponding ledger update. Fixed by listing all 6 in one pass. T17 audit also fixed the T17-1a vitest that claimed to assert ADR-0041 S1 region guard but never switched viewMode off the default subscription path — the storage-seam pre-populate pattern (T15-3 precedent at L635-644 of `TopologyView.test.tsx`) is now used to hydrate region viewMode before render. +6 source hits on 2026-08-17 (was 2 on 2026-08-13). Five keep, one done. No stale markers remain. Drift surfaced in the post-T17 audit: the T15 + T16 + T17 commits added 4 keep markers (`commands/mod.rs` L908 poison-safe mutex, `sidecar.rs` L330 Job Object race, `sidecar.rs` L882 notification crate, `TopologyView.tsx` L763 S2 helper deferral) without a corresponding ledger update. Fixed by listing all 6 in one pass. T17 audit also fixed the T17-1a vitest that claimed to assert former ADR-0041 (retired 2026-10-15) S1 region guard but never switched viewMode off the default subscription path — the storage-seam pre-populate pattern (T15-3 precedent at L635-644 of `TopologyView.test.tsx`) is now used to hydrate region viewMode before render. ## Historical note on AGENTS.md §51 (T7 diff) diff --git a/docs/README.md b/docs/README.md index 61376e90..742c9aa5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,17 +5,15 @@ | Directory | Purpose | Diataxis quadrant | |---|---|---| | `architecture/` | System architecture, design decisions | Explanation | -| `adr/` | Architecture Decision Records (68 ADRs: 67 numbered + 1 bis, MADR format) | Reference (decisions) | +| `adr/` | Architecture Decision Records (55 ADRs, MADR format; numbering 0001–0080 with gaps — 26 bookkeeping records retired 2026-10-15, recoverable via git history; `0050` exists twice as `0050` + `0050-bis`) | Reference (decisions) | | `how-to/` | Operational guides (deployment, release, runbook) | How-to | | `reference/` | Glossary, API references | Reference | | `research/` | Research findings, protocol analysis | Explanation | -| `history/` | Completed phase plans, handoffs, prompt archives | Archive (not current) | -| `DOCUMENT_GOVERNANCE_PLAN.md` | This docs structure governance plan | Constraint (top-level) | | `PONYTAIL_DEBT_LEDGER.md` | Active ponytail debt tracking | Constraint (top-level) | | `RESIN_UPSTREAM_MANIFEST.yaml` | Resin upstream version manifest | Reference (top-level) | ## Conventions - All migrations use `git mv` (preserves history, R100 = content unchanged). -- Constraint docs stay at `docs/` top level; history artifacts move to `docs/history/`. -- For the full governance plan, see `DOCUMENT_GOVERNANCE_PLAN.md`. +- Constraint docs stay at `docs/` top level. Retired docs are deleted, not archived in-tree — history lives in git. +- `.scratch/` holds session-local governance working files (gitignored). diff --git a/docs/adr/0001-shell-forwarding-vs-resin-webui-port.md b/docs/adr/0001-shell-forwarding-vs-resin-webui-port.md deleted file mode 100644 index fa709a99..00000000 --- a/docs/adr/0001-shell-forwarding-vs-resin-webui-port.md +++ /dev/null @@ -1,125 +0,0 @@ -# ADR-0001: Keep shell-forwarding architecture, deepen IPC coverage - -Date: 2026-08-02 -Status: PROPOSED (pending user confirmation in grill-with-docs session) -Decision Type: Architectural - -## Context - -The project forks Resin (github.com/Resinat/Resin) as a Go sidecar inside a -Tauri 2 + React 19 desktop shell. The original G4 spec called for physically -copying Resin/webui/ React components into src/resin-views/. The P16 Ponytail -decision skipped that physical port and instead built a self-contained 5-tab -desktop shell with ResinClient + IPC forwarding to the live sidecar. - -## Decision - -Keep the shell-forwarding architecture (self-built 5-tab + ResinClient IPC). -Do NOT physically port Resin/webui/ React components. - -Deepen IPC coverage for 4 specific gaps identified in the Q1 gap audit: -1. Subscription enable/disable + interval + ephemeral toggle + LastError display -2. Reverse-proxy header-rule editor (empty_account_behavior + fixed_account_header) -3. Structured request-log viewer tab (per platform/account/target filtering) -4. Passive circuit-breaker toggle exposed via platform PATCH - -## Rationale - -Physical port conflicts: -- Resin's webui has its own React/Vite/TS stack, state management, routing, - and i18n directory. Embedding it as sub-views introduces two parallel React - apps, two routing models, two i18n catalogs. An iframe wrapper breaks IPC. -- The desktop shell already owns: Tauri webview, system tray, theme switching, - i18n (18 locales via react-i18next), topology canvas (ReactFlow), Ghost safety - net. A full React app piggyback would either bypass all of those or need - rewriting each Resin component to fit. - -The architectural contract is sound: shell sees live Resin data via -ResinClient REST forwarding. The gap is IPC coverage depth, not structure. - -## Consequences - -- 39 Resin admin endpoints remain the only way the webview reaches Resin. - The admin token stays Rust-side. No new domain crossing. -- 4 new IPC commands needed (subscription_update, request_log_query, - reverse_proxy_config_get/set, platform circuit-breaker toggle). -- Each new view tab remains a native desktop view, not a foreign React app. - -## UPDATE (2026-08-02, after user Q1 reply) - -User answered Q1 = (C): "internally audit and decide between (a) bringing the -Resin mature webui IN and merging our shell into it, vs (b) keeping our 5-tab -shell and only back-porting the missing views from the Resin webui." User -added a pivotal requirement: - -> 我以后可能不只是考虑本机的 Tauri 壳子,因为我以后是想要把这个 -> 搬进一些 VPS 里面的,这种一般都需要稳定的后端和 localhost 的前端 - -This bifurcates the deployment surface: -1. Desktop shell path: Tauri webview embeds the React app built to ../dist (no - separate frontend server, no localhost:80 port). -2. VPS path: needs a standalone backend (the Resin Go binary itself is the - backend) + a localhost-served frontend. The current Resin Go binary already - serves its own webui at http://127.0.0.1:2260 (per README) inside the same - port that carries the control-plane API + the proxy — i.e. for VPS use you - can just deploy the Resin Go binary alone and point your browser at its - webui. The Tauri shell is the DESKTOP convenience layer, not the only - frontend. - -### Internal deliberation - -(a) Merge Resin webui into the desktop shell — physically port - vendor/resin/webui React components into src/, rewrite their API client to - use Tauri IPC instead of fetch(), rewire their state mgmt, port their - routing, port their i18n strings, and abandon our existing Topology canvas - / ProcessRoute / Settings / tray / theme work. Pros: we get Resin's - Platform/Subscription/Lease/Metrics/Log views verbatim. Cons: - - Two parallel React stacks must be merged; their i18n catalog and ours - must be reconciled across 18 locales × ~150 keys, per-key. - - Their API client hits fetch(:2260) — we MUST rewrite every call site to - invoke() or a flag-day dual-mode shim; the desktop path needs IPC for - the admin token to stay Rust-side (AGENTS §7.6). - - The VPS path doesn't need any of this — Resin's own webui is already - served standalone on the sidecar port. Merging webui into src/ helps - only the desktop path, and even there the cost is a large rewrite that - loses our Topology / ProcessRoute / tray work. - - Loses our USPs: topology canvas, ProcessRoute, Ghost safety net, - backup-WebDAV, single-instance, tray i18n, locale memory — Resin's own - webui has none of these. - -(b) Keep our 5-tab shell + selectively back-port missing Resin webui views. - Pros: - - Desktop path keeps its UX USPs. - - VPS path: just ship the Resin Go binary standalone (it serves its own - webui at :2260) + our optional Rust shell as a desktop add-on. No merge - cost, no dual-mode shim, no i18n reconciliation across two catalogs. - - Our IPC forwarder is the SINGLE source of truth for what the desktop - user can do; we choose which Resin features to surface. - Cons: - - We must hand-write the vewing ports one by one (subscription_update, - request_log_query, node-pool depth, reverse-proxy header rule editor, - circuit-breaker toggle). Each is ~one new ResinClient method + one new - IPC command + one view extension, matching our existing 36-command - shape. Not zero work, but bounded and additive. - -### Decision - -Path (b): keep our 5-tab shell; selectively back-port the missing views via -new IPC commands. The VPS path delegates the frontend to the Resin Go -binary's own bundled webui (no extra work — Resin already serves it). - -This is the cleanest split: -- Desktop: one React app, one IPC surface, admin token never in webview. -- VPS: deploy the Resin Go binary alone (or via our release pipeline as a - standalone tarball). The Resin webui on :2260 IS the VPS frontend. - -### What this ADR now blocks - -- Physical port of vendor/resin/webui into src/ — REJECTED for the desktop - path (we keep the shell-forwarding contract). The Resin webui remains the - authoritative VPS deployment frontend served by the sidecar itself. -- Any dual-mode shim or shared React component library ambitions between our - shell and Resin's webui. The back-port is selective and bespoke, not a - shared library. - -Status: ACCEPTED (pending user confirmation of this internal decision). diff --git a/docs/adr/0002-topology-canvas-three-column.md b/docs/adr/0002-topology-canvas-three-column.md deleted file mode 100644 index bfc0d391..00000000 --- a/docs/adr/0002-topology-canvas-three-column.md +++ /dev/null @@ -1,57 +0,0 @@ -# ADR-0002: Three-column topology canvas (A/B/C) maps to Resin region_filters - -Date: 2026-08-02 -Status: PROPOSED (provisionally resolved from evidence; pending user grill) -Decision Type: UX + Architecture - -## Context - -User requirement #5: three columns A (entry proxy port), B (api-key -platforms), C (ip/ip channels). A-to-B always connected; B drags to C to bind -egress. The topology canvas is the visual hot-edit surface for routing. - -## Live-probed Resin behaviour (2026-07-31, 33 real nodes) - -- Platform-to-node binding is via region_filters (region codes), NOT per-node. - Default platform (no filters): routable = all healthy. - Platform with region_filters:["hk"]: routable = 3 HK nodes only. - Platform with regex_filters:["api.openai.com"] but no region_filters: - routable = 0 (regex matches upstream REQUEST hosts, NOT nodes). -- A non-Default platform with no region_filters gets 0 routable nodes — it - must specify a region to be useful. - -## Decision - -Keep the three-column A/B/C canvas: -- A = single conceptual "entry proxy port" node (the shell forwards all - traffic to a single Resin forward proxy listener; visually one node). -- B = one node per live Resin platform from GET /api/v1/platforms. -- C = node-region GROUPS from GET /api/v1/nodes (collapsed by region tag). -- B-to-C edges = platform.region_filters includes that region code. -- Dragging a new B-to-C edge = PATCH the platform region_filters live. -- Deleting an edge = PATCH region_filters to remove that region. - -## Rationale - -This maps 1:1 to Resin's native contract. Every visual edge IS a real config -field on a real Resin entity. Hot PATCH on drag = immediate config edit. -Backup issued before every drag PATCH (P19 item 4) so changes are reversible. - -## What this is NOT - -- It is NOT a per-node binding. Resin does not expose per-node selection. -- It is NOT a key-to-lane-slot mapping. The old Lane concept is a shell-only - UI artefact; the actual routing is Platform.Account->Lease->Node, owned - entirely by Resin's in-process Go runtime. -- It does NOT promise bandwidth/sequential/random egress policies. Resin - v1.1.2 exposes exactly three allocation_policy values (BALANCED, - PREFER_LOW_LATENCY, PREFER_IDLE_IP); see ADR-0003. - -## Consequences - -- The canvas UI is honest: every visible edge corresponds to a Resin config - field. No visual element floats without backend semantics. -- The user's requested "single IP / multi IP strategy" maps to: - - single IP = region_filters with one region, sticky_ttl long enough. - - multi IP = region_filters with multiple regions, allocation_policy = - BALANCED or PREFER_LOW_LATENCY (Resin rotates within eligible nodes). diff --git a/docs/adr/0004-egress-policy-surface.md b/docs/adr/0004-egress-policy-surface.md deleted file mode 100644 index 21a26458..00000000 --- a/docs/adr/0004-egress-policy-surface.md +++ /dev/null @@ -1,49 +0,0 @@ -# ADR-0004: Egress policy surface = three Resin-native values; document the gap - -Date: 2026-08-02 -Status: PROPOSED (provisionally resolved from evidence) -Decision Type: Feature scope - -## Context - -User requirement #3 / item three requested: fully random / sequential / by -latency / by bandwidth / by quality-weight / by protocol-weighted quality. - -## Decision - -Surface exactly the three Resin-native allocation_policy values: -- BALANCED (shell label: "balanced / quality weight" - Resin's native default) -- PREFER_LOW_LATENCY (shell label: "by latency") -- PREFER_IDLE_IP (shell label: "by idle IP") - -Document the gap: the user's asked random / sequential / bandwidth / -protocol-weighted are NOT resembling Resin-native values in v1.1.2. -The shell does NOT implement them as shell-side logic overriding Resin -(such a layer would fight Resin's own selection and break lease stickiness). - -## What we DO surface - -- ipChannelPolicySet wrapper maps five UI labels to the three Resin values: - "random" / "sequential" -> BALANCED - "latency" -> PREFER_LOW_LATENCY - "quality" -> PREFER_IDLE_IP - "bandwidth" -> BALANCED (documented limitation) - -- docs/PROTOCOL_WEIGHT_RESEARCH.md already documents how the outbound node - protocol affects AI SSE/WebSocket (http=1.0, ss=0.7, hysteria2=0.1). This - is advisory reference, NOT a runtime-injected weight. - -## What we DO NOT do - -- Do not inject a custom shell-side egress selector over Resin's runtime. -- Do not promise protocol-weighted selection; Resin has no such endpoint. -- Do not hide the limitation from the user. The Nodes view shows an - egressPolicyNote (i18n) pointing the user to the Topology canvas where - allocation_policy binds; the info card documents the protocol weight table. - -## Consequences - -- Users with a hard requirement for true random / sequential / bandwidth must - fork Resin to add those policies, or wait for a future Resin release. -- The shell reporting is honest: the GUI exposes what Resin actually supports, - nothing synthetic. diff --git a/docs/adr/0005-q5-q9-cluster.md b/docs/adr/0005-q5-q9-cluster.md deleted file mode 100644 index f4808543..00000000 --- a/docs/adr/0005-q5-q9-cluster.md +++ /dev/null @@ -1,127 +0,0 @@ -# ADR-0005: Q5-Q9 resolved - subscription depth, log viewer, CI/CD, backup, tests - -Date: 2026-08-02 -Status: PROPOSED (provisionally resolved from evidence) -Decision Type: Multi-decision (one ADR per cluster; consolidated) - -## Q5: Subscription management depth - -### Evidence (resin_client.rs source) -ResinClient exposes: create_subscription, list_subscriptions, delete_subscription. -No subscription_update PATCH method exists in the .rs file. - -### Gap -Resin subscriptions table carries: enabled, update_interval_ns, ephemeral, -LastError (display). Webui lets the user toggle enabled and set the interval. -Our SubscriptionsView only does add/list/remove (import a Clash URL + delete -by name). No enable/disable, no interval control, no ephemeral toggle, no -LastError display. - -### Decision -Add ipcSubscriptionUpdate(name, {enabled?, update_interval?, ephemeral?}) -that PATCHes /api/v1/subscriptions/{id} (probe the endpoint shape on the live -sidecar before assuming). ResinClient::update_subscription(id, body) method. -SubscriptionsView extends the per-row card with a Enable toggle and an -Interval select (30s/5m/15m). The LastError column is read-only; surface the -field from list_subscriptions. - -### Status -GAP — implement in next phase after user confirms grill direction. - -## Q6: Request log viewer - -### Evidence -There is no request-log query IPC, no LogViewer tab, no ResinClient method -that queries /api/v1/logs or similar. The shell Rust tracing layer writes to -tauri-plugin-tracing's daily-rotating files; that is Rust-shell trace, not -Resin's structured request log (the user-visible audit trail). - -### Gap -Resin's README explicitly advertises "complete structured request logs for -querying and auditing by platform, account, target site, and more." This is -a major user-visible feature we surface nowhere. - -### Decision -Add a new "RequestLogs" tab (or sub-view of Topology). ResinClient gains a -query_request_logs(filters) method that GETs the correct Resin endpoint -(DESIGN.md references a structured request log but no endpoint was probed -live; probe the sidecar for GET /api/v1/logs?platform=&account=&target= before -coding). The view renders filterable rows with timestamp / platform / account -/ target / status / egress_ip. - -### Status -GAP — DESIGN.md confirms the feature exists in Resin; the public admin API -endpoint shape must be live-probed before code. - -## Q7: CI/CD completeness - -### Evidence (ci.yml, build-all.sh) -- CI matrix: backend (linux x86_64, windows msvc, macos arm64, macos x86_64). -- Installer matrix (msi/nsis setup.exe, deb+AppImage, dmg for arm64+x86_64). -- Portable matrix (linux/windows msvc/macos arm64 — macos x86_64 portable IS - present via the matrix.include list in the ci.yml gui-portable job). -- Every GUI job fetches the Resin sidecar via fetch_resin.sh before tauri build. -- build-all.sh reproduces locally: pnpm build -> cargo build release -> stage - release/-gui/ with bundles + portable ai-api-route(.exe). Flow guard - on windows launches the portable exe and verifies MainWindowTitle, 5s alive. - -### Decision -CI/CD satisfies the user's release pipeline requirement (backend + installer + -portable per OS, fetch-sidecar guard, ERR_CONNECTION_REFUSED prevention via ---features custom-protocol). No change required for Q7. - -### Status -COMPLETE as-evaluated. User previously noted the stale-release-exe hazard -(P23) — AGENTS.md §5 was hardened to forbid bare cargo build without vite build -first. That is the only durable fix needed; CI itself is fine. - -## Q8: Backup/WebDAV security - -### Evidence (commands/mod.rs) -- backup_create zips settings.json + Resin state dir to backups_dir with a - crypto-random suffix. -- backup_upload(url, username, password, zip_path) uploads to WebDAV via PUT. - P14 fix: canonicalize zip_path + starts_with guard confines to - app_data/backups - blocks the traversal bug. -- backup_list(url, username, password) does PROPFIND on the WebDAV base. -- config_import auto-creates a backup before applying (防呆). - -### Decision -Backup is shipped with the P14 path-traversal fix in place. The only -remaining security note (P22 audit): WebDAV credentials + keyCandidates -apiKey are stored plaintext in settings.json (tauri-plugin-store). Known -risk, mirrors the clash-verge-rev pattern. Not upgrading to OS keyring in -this grill cycle. - -### Status -COMPLETE. Security is audited and the documented risk is accepted. - -## Q9: Test coverage - -### Evidence -- vitest files (8): appStore.test.ts, ipc.test.ts, subDrag.test.ts, - TopologyView.test.tsx, PlatformsView.test.tsx, SubscriptionsView.test.tsx, - ProcessRouteView.test.tsx (plus a couple). 66 tests pass. -- e2e tests: e2e directory is empty (no .ts files). CI runs playwright test - against the Vite dev server surface; local playwright skipped as host - resource. -- resin_client.rs has mockito-based offline integration tests covering - create/list/delete/error for platforms + subscriptions (6 tests + update). - -### Gap -The closed-loop coverage described in AGENTS §18 "each behavior module must -have a closed-loop test" is satisfied for the IPC boundary and for the few -pure helpers (subDrag, applyOrder), but the views themselves only mock -invoke() — they do not cover the Topology drag-to-PATCH end-to-end through -a live or mockito mock. There is no integration test that asserts -"add-platform -> list-platform -> the new platform appears in Topology". - -### Decision -Acceptance: ship the grill with the existing test shape. The closed-loop -contract means "each IPC boundary has vitest asserts that the wrapper exists -and validates input + forwards to invoke". Future phases should add an -e2e suite that drives IPC behavior through a mockito Resin mock and asserts -visual state change — but this is a longer-horizon need, not a Q9 grill blocker. - -### Status -ACCEPTED with documented gap. Not a blocker for moving to implementation. diff --git a/docs/adr/0006-mainline-a-then-b.md b/docs/adr/0006-mainline-a-then-b.md deleted file mode 100644 index 71ed54c5..00000000 --- a/docs/adr/0006-mainline-a-then-b.md +++ /dev/null @@ -1,62 +0,0 @@ -# ADR-0006: Q5 completion criterion = mainline A (function closure) then mainline B (release standardization) - -Date: 2026-08-02 -Status: ACCEPTED -Decision Type: Completion criterion + work sequencing - -## Context - -Q5 of the grill-with-docs session asked: what is the completion criterion for -this project right now? Two mainlines were proposed: - -- Mainline A (function closure first): "software does not look like a toy" = - wire the last GUI-to-real-Resin-data breakpoints; release can wait. -- Mainline B (release closure first): ship a traceable v0.1.0 = fill - release/ with real Linux/macOS GUI + backend tarballs, then tag. - -## Decision - -Mainline A first, then mainline B. The two are sequential, not parallel. -A is the precondition for B: shipping a tagged release while the GUI still -has hollow data breakpoints would ship a toy with a version number, which -contradicts the user is repeated "not a toy" demand. - -Q6 confirmed mainline A scope = four items: -1. platform_snapshot wired to real GET /platforms/{id}/routable-view - (HANDOFF_PATH_A_CONTINUATION next-deep-work-1). -2. subscription import closed-loop audit-then-fix (the user test URL - https://link123.52pokemon66.cc/... must produce visible nodes end-to-end). -3. (Item 4 in Q6 numbering) A4-3 interceptor validated against the REAL - Resin sidecar (not just mockito) -- two distinct (api_key, endpoint) pairs - produce two distinct X-Resin-Account headers -> two distinct leases -> - two distinct egress IPs, proven with the live binary. -4. (Item 5 in Q6 numbering) tray context menu i18n syncs immediately when - the GUI locale changes (no restart, no half-beat lag). - -## What is NOT in mainline A - -- Node-per-account editor (YAGNI: Resin v1.1.2 has no node-level account - CRUD endpoint; user has not named this requirement). -- build-all.sh externalized sidecar fetch (hygiene, not a closure gap). -- Keyboard nav / per-view caching (user never named these; speculative). -- v0.1.0 tag (mainline B, after A closes). - -## Relationship to prior ADRs - -- ADR-0003 authorized the A4-3 interceptor (Q4 answer). This ADR-0006 item 3 - extends ADR-0003 is closed loop from mockito-only to live-sidecar e2e. -- ADR-0005 Q5 (subscription management depth) overlaps this ADR-0006 item 2. - item 2 is the audit-then-fix path; ADR-0005 Q5 is the broader feature - (enable/disable/interval/LastError). They converge: item 2 verifies the - existing import path; ADR-0005 Q5 extends it. -- ADR-0005 is PROPOSED. This ADR-0006 depends on ADR-0005 being implemented - eventually but does not block items 1/3/4 of mainline A. - -## Consequences - -- Mainline B (release standardization) does not start until all four - mainline A items have closed-loop tests passing. -- Each mainline A item ships with: cargo test + vitest closed-loop, release - exe rebuilt, codegraph sync, AGENTS.md section appended, commit+push. -- Glossary terms: "closed-loop test", "live-sidecar e2e", "routable-view" - defined in docs/glossary.md (this ADR pair creates it). diff --git a/docs/adr/0007-subscription-last-error-surface.md b/docs/adr/0007-subscription-last-error-surface.md deleted file mode 100644 index 1ebbdb7d..00000000 --- a/docs/adr/0007-subscription-last-error-surface.md +++ /dev/null @@ -1,77 +0,0 @@ -# ADR-0007: ADR-0006 item 2 closed-loop audit-then-fix (subscription last_error surface) - -Date: 2026-08-02 -Status: ACCEPTED -Decision Type: IPC projection + GUI observability - -## Context - -ADR-0006 item 2 audit-then-fix on the subscription import closed-loop. -Two facts emerged from live-sidecar probes: - -1. The old user test URL `https://link123.52pokemon66.cc/...token=79e12...` now - returns 403 to EVERY tested UA (clash-verge/v2.0.0, clash/v1.18.0, - ClashForWindows/0.20.39, mihomo/1.18.0, plus Resin own default UA). - Resin itself reports `last_error: downloader: unexpected status 403`. - So the URL is source-side dead for this exit IP; the P13 fix pattern - (shell-side clash-family UA + flow->block convert + local POST) is still - correct and was re-verified to parse 3 local fixture proxies > 3 nodes. - -2. The new URL `https://jinxi2410.qzz.io/cfnew2/sub?target=clash` 200s + - 73276 bytes under clash-verge/v2.0.0; raw local-POST to Resin yields - `node_count=102` within 12s. End-to-end real loop verified. - -The actual hollow point is NOT the import path (it works); it is the IPC -projection. `SubscriptionSnapshotEntry { name, node_count }` dropped -Resin `last_error` / `last_checked` / `healthy_node_count`, so -when a user imports a URL whose source returned 403 the GUI showed a mute -"0 nodes" with no reason. That is the toy UX ADR-0006 item 2 targets. - -## Decision - -Extend `SubscriptionSnapshotEntry` with three fields: - - `healthy_node_count: u64` (default 0) - - `last_error: string` (default empty; Resin uses empty string not null, but the helper handles null too) - - `last_checked: string` (RFC3339 timestamp) - -The GUI renders a secondary line below each subscription row showing -healthy count, a trimmed last_checked timestamp, and (when non-empty) the -last_error text in a rose-tinted span. last_error is technical telemetry and -is rendered verbatim > no translation. healthy count uses the i18n label -`subscription.healthy` as a tag, but the count itself is the literal -number so a fetch 403 cannot masquerade as a mute 0 in any locale. - -## Consequences - -- A future source 403 / DNS fail / parse error is visible at the row level - without restarting the GUI or inspecting the log file. -- The `SubscriptionSnapshotEntry` struct widened; the TS interface - mirrors it. Existing tests that asserted only name+node_count still pass - (the new fields default to 0/empty when Resin omits them). -- AGENTS S31 scope remaining after item 2 closed: items 3 (A4-3 live-sidecar - e2e) and 4 (tray i18n half-beat). - -## Closed-loop tests - -- `subscription_snapshot_reads_resin_items_wrapper_with_node_count`: - extended to assert defaults when Resin omits the three new fields, plus - the non-empty last_error path. -- `subscription_snapshot_handles_null_last_error_and_missing_healthy`: - Resin (or any Go encoder) might emit `null` for an unset string - pointer; `.as_str().unwrap_or("")` collapses null to empty. -- `SubscriptionsView item 2 row hint`: `shows last_error in red`, - `shows healthy count when >0 and trims last_checked sub-seconds`. - Vitest jsdom does not eager-load the i18n resources-to-backend chunks, so - the matcher allows the raw-key fallback AND classList-based assertions - (the CSS-selector `.text-rose-600` matches the `dark:text-rose-400` - modifier incorrectly in jsdom). -- `cargo test -p resin-core --lib` = 79 passed; `pnpm test` = - 76 pass / 8 files (item 2 added 2 new tests; was 74). - -## Build - -`cargo build --release -p ai-api-route-app --features custom-protocol` >> -11.46MB exe staged at `release/windows-gui/ai-api-route.exe` + -`resin.exe` sidecar (37.8MB gitignored). Embedded latest Vite chunk -`index-CfSStSwB`. Smoke: MainWindowTitle=ai-api-route, WS 36.4MB, -resin child alive. diff --git a/docs/adr/0008-item3-live-sidecar-e2e.md b/docs/adr/0008-item3-live-sidecar-e2e.md deleted file mode 100644 index 8fed5c00..00000000 --- a/docs/adr/0008-item3-live-sidecar-e2e.md +++ /dev/null @@ -1,139 +0,0 @@ -# ADR-0008: ADR-0006 item 3 live-sidecar e2e (X-Resin-Account honored by real Resin binary) - -Date: 2026-08-02 -Status: ACCEPTED -Decision Type: Live-sidecar e2e closed loop + URL format correction - -## Context - -ADR-0006 item 3 is the live-sidecar end-to-end closed loop for the A4-3 -interceptor (crates/resin-core/src/interceptor.rs), which sits between an -upstream AI gateway (omniroute / litellm) and the Resin Go sidecar reverse -proxy. The interceptor computes `route_id` (FxHash three-tuple of -normalized auth value + JSON body.model + request path) and injects -`X-Resin-Account: ar-<16hex>` before forwarding, so the same upstream key -calling two different upstream endpoints (model + path) yields two distinct -Account identities and thus two distinct egress IPs inside Resin. - -Until this ADR the only closed loop for the interceptor was a mockito unit -test that proved distinct (key, model, path) triples yield distinct -`X-Resin-Account` values on the forwarded request. There was no evidence -the real Resin Go binary (v1.1.2) actually honors the injected header in -its live routing path. ADR-0006 item 3 closes that gap with a live-sidecar -e2e test that spawns the real Resin binary and observes its behavior on -two distinct (key, endpoint) pairs. - -## Correction applied during the loop - -The first live run returned `400 Protocol must be http or https`. Root -cause: the interceptor forward URL was `/{token}/https/{host}{uri}`, which -omits the mandatory identity path segment between the token and the -protocol. Resin DESIGN.md (master branch, reverse-proxy surface) requires -the path format `/////`; the identity -segment is mandatory. The fix lands an identity segment of `Default` (the -Resin fallback platform) so the URL parses. The injected -`X-Resin-Account` header takes routing precedence over the URL identity -segment, so the route_id-derived id still drives Account selection. - -Interceptor.rs forward URL before the fix: -``` -{resin_base}/{proxy_token}/https/{host}{uri} -``` -after the fix: -``` -{resin_base}/{proxy_token}/Default/https/{host}{path_and_query} -``` - -The inline comment block in interceptor.rs was updated to record the -authoritative format and the identity-segment rationale. - -## Test design (crates/resin-core/tests/a4_3_live.rs) - -The live e2e test is a labelled `#[ignore]` integration test. It runs only -when explicitly requested: -``` -cargo test -p resin-core --test a4_3_live -- --ignored --nocapture -``` -This keeps the default `cargo test` suite green on hosts without the Resin -binary and lets a developer with the sidecar staged prove the loop alive. - -The test: - -1. Allocates a free loopback port, generates admin + proxy tokens (32 hex - chars), resolves a per-test temp state/cache/log dir trio, and spawns - the Resin Go binary with the same env contract as the Tauri shell - (RESIN_AUTH_VERSION=V1, RESIN_ADMIN_TOKEN, RESIN_PROXY_TOKEN, - RESIN_LISTEN_ADDRESS, RESIN_PORT, RESIN_STATE_DIR, RESIN_CACHE_DIR, - RESIN_LOG_DIR). -2. Polls /healthz until the control plane is reachable (15s deadline). -3. Sends two probe requests through the interceptor with distinct - (auth, body.model, path) triples: - - pair A: Bearer sk-test-key-alpha + {"model":"gpt-5.6"} + /v1/chat/completions - - pair B: Bearer sk-test-key-alpha + {"model":"claude-sonnet-5"} + /v1/chat/completions - (same key, different upstream endpoint identity -> two distinct - route_id -> two distinct X-Resin-Account injected values). -4. Reads the live leases endpoint GET /api/v1/metrics/realtime/leases and - classifies each row as either an aggregate row (no account / egress_ip - fields) or a per-key row (has account + egress_ip). -5. Strong pass path: when per-key rows exist, assert at least one row's - account prefix matches the injected id; strong fail otherwise. -6. Soft pass path: when only the aggregate row is present (the dev host - has no subscription nodes so Resin returns 503 No available nodes for - routing after parsing the request) the test still PASSes because the - proof target is narrower: the URL parsed correctly, Resin accepted the - X-Resin-Account header injection (no 400/protocol error), and the - request reached the routing layer. Per-key binding is a Go-internal - invariant by design, not exposed via the leases endpoint. - -## Live evidence (honest, not self-deception) - -``` -a4_3_live: resp_a status=503 Service Unavailable body[:200]=No available nodes for routing -a4_3_live: resp_b status=503 Service Unavailable body[:200]=No available nodes for routing -a4_3_live: expected X-Resin-Account A = ar-9235878de92e0565 -a4_3_live: expected X-Resin-Account B = ar-6d5833d658a96baa -a4_3_live: live leases endpoint returned 1 item(s) -a4_3_live: lease[0] = {"active_leases":0,"ts":"2026-08-02T15:11:15.2897376Z"} -a4_3_live: lease breakdown = 1 aggregate, 0 per-key entries -a4_3_live: SOFT PASS (dev host has no node) -test a4_3_live_resin_honours_x_resin_account ... ok -``` - -Each prior failed URL format returned 400 (URL parse-level reject) - the -transition from 400 to 503 is the proof the corrected URL is parsed by -Resin and the request reached the routing layer. 503 No available nodes -is the expected Resin behavior on a dev host with no subscription nodes; -the routing layer cannot select an egress because the node pool is empty. - -The per-key lease surface is intentionally not exposed by Resin via the -leases endpoint (DESIGN.md: per-key binding is an internal Go sidecar -invariant). The mockito unit test -`a4_3_distinct_key_endpoint_yields_distinct_egress` in interceptor.rs -covers the per-key identity injection contract at the request-boundary -layer; that test asserts the two forwarded requests carry distinct -X-Resin-Account values computed from the same two (key, model, path) -triples the live test sends. - -## Decision - -- The live-sidecar e2e test is the authoritative closed loop for ADR-0006 - item 3. It is ignored by default (no host dependency on the Resin - binary) and explicit opt-in via `--ignored`. -- The interceptor forward URL format is fixed to insert the identity - segment (`Default`). The fix is a one-line string concat change in - interceptor.rs; the inline comment documents the DESIGN.md contract. -- The soft-pass path documents honestly that the dev host's per-key lease - surface is not observable; it is not claimed as a hard proof of per-key - egress IP isolation. The hard proof lives in the mockito unit test. -- ADR-0006 item 3 is closed. - -## Consequences - -- Any future change to the interceptor forward URL format must keep the - identity segment or the live test will regress to 400. -- The ResinClient / leases endpoint shape (aggregate row with - active_leases + ts, no per-key fields) is now an explicit test - invariant; a future Resin API change that adds per-key fields should - flip the strong-pass branch on. -- The test is host-scoped: it must not run in CI without the Resin binary - staged at the expected per-triple path. The ignored flag is the guard. diff --git a/docs/adr/0009-cli-gui-alignment-status.md b/docs/adr/0009-cli-gui-alignment-status.md deleted file mode 100644 index 21b79838..00000000 --- a/docs/adr/0009-cli-gui-alignment-status.md +++ /dev/null @@ -1,124 +0,0 @@ -# ADR-0009: CLI binary vs GUI IPC surface alignment status (Nightly domain-modeling review) - -Date: 2026-08-03 -Status: ACCEPTED -Decision Type: Domain-modeling audit finding + deferred refactor decision - -## Context - -A Nightly domain-modeling review flagged a contradiction between user -vocabulary and code: the user said "synchronise all GUI and CLI functions -for alignment, and carefully handle the CLI command set" — implying the -CLI binary and the GUI binary expose a parity surface. The code reality is -starker. - -The repository ships two cargo binary targets plus one OS sidecar: - -- `src-tauri/src/main.rs` → `ai-api-route-app` → `ai-api-route.exe`: a - Tauri 2 desktop shell that registers 33 `#[tauri::command]` IPC - handlers (platform / subscription / node / lease_map / interceptor / - backup / config / process_route / gateway / tray). It also `boot_resin`s - the Go sidecar and drives the axum interceptor (route_id -> X-Resin-Account - injection) from inside `.setup()`. The webview reaches Resin only via the - Rust IPC layer (admin token stays Rust-side, AGENTS §7.6). -- `crates/resin-core/src/bin/resin_core.rs` → `resin-core` → `resin-core.exe`: a 31-line stub. It parses `--config ` with clap - but binds it to `let _args = Args::parse()` (underscore = unused). It then - constructs `CoreConfig::default()`, ignores everything, logs a single - `tracing::info!` "resin-core starting (headless stub)", and returns Ok. - The file's own comments admit "the Tauri shell orchestrates the full - gateway lifecycle in production" — i.e. the binary is currently a - placeholder so the CI backend target compiles, not a parity surface. -- `src-tauri/binaries/resin-x86_64-pc-.exe` (gitignored): the Go - Resin v1.1.2 binary the Tauri shell spawns as a sidecar. - -The contradiction: the user (ADR-0001 UPDATE) intends to split deployment -into a Desktop path (Tauri webview embeds dist) and a VPS path ("needs a -stable backend + localhost frontend"). The VPS path is not implemented. -The `resin-core` binary is the natural anchor for it, but today it is -an empty shell. Citing "synchronise GUI and CLI" presumes work that has -not happened. - -## Web research (1mcp exa `web_search_exa` on the dual-mode Rust scaffold space) - -Three relevant wheels surface; we pick none in this ADR but record them -to avoid re-research later. - -1. `yolorouter/dualkit` — "Open-source Rust dual-mode application - scaffolding: one codebase, two forms – desktop GUI and headless - service". Two cargo bin targets (`dualkit` GUI + `dualkit-headless` - axum server), one `dualkit-domain` crate exposes the same handlers - at `POST /rpc/:command` and as Tauri IPC. The frontend's - `@dualkit/shell` client auto-detects HTTP vs IPC. Flag set: - `--headless --lan 0.0.0.0:8080 / --local-only / --bind /`. - This is the closest structural match for ADR-0001's bifurcated - deployment goal and the largest refactor (new crate layout). -2. `Nahida-aa/fnrpc` — "A single router serves both Axum (HTTP SSE) and - Tauri (IPC). TypeScript types generated automatically via specta." - Macro-driven `#[rpc_query] / #[rpc_mutate] / #[rpc_subscribe]` drive - both transports from one Rust router. Adds a specta codegen step and - a per-handler macro dependency. -3. `dohooo/helmor`'s `src-tauri/src/companion/server.rs` — a - type-erased `Dispatcher = Arc BoxFuture>> that forwards `POST /rpc/{cmd}` to the real Tauri command behind the - concrete AppHandle, so commands needing `State` / `AppHandle` work. - The smallest-surprise port for this repo if we ever wire parity: it - keeps the 33 `#[tauri::command]` as-is and adds ONE dispatcher - layer. The cost is a runtime dispatch indirection and a custom auth - boundary (bearer token + loopback + Host/Origin check per ADR-0022 - of that project) we do not have today. - -The Tauri v2 official `plugins.cli` was also evaluated. It attaches clap -subcommand parsing to the DESKTOP binary, not a standalone headless -binary. It does not address the VPS deployment path and is not the right -tool for parity here. - -## Decision - -1. ACCEPT the contradiction: the `resin-core` CLI binary is currently a - stub, NOT a parity surface for the GUI. This ADR records the finding so - a future agent can no longer falsely claim "the CLI is aligned with - the GUI" by reading the cargo manifest alone (32 IPC commands would - appear to exist in both but the CLI implements none of them). -2. DEFER the parity refactor to a grill Q7+ VPS-phase decision. ADR-0001 - already tracks the VPS deployment goal. The structural candidate is - the dualkit / helmor dispatcher model (wheels 1 / 3 above); it should - not be implemented in a Nightly maintenance window because it is a - multi-crate refactor and an ADR-0001-level architecture decision. -3. CAREFULLY EXTEND the CLI stub's instruction set in THIS commit so the - stub honestly announces itself and reads `--config` for real, instead - of silently dropping it. This is a Ponytail-grade fix: no new feature, - no parity claim, no large refactor — only "the binary stops lying - about what it does". A future VPS-phase agent replaces this stub - entirely; the extension below is the smallest change that survives a - code review and stops a confused user from concluding the CLI has - functions it does not. - -## CLI instruction-set extension (in this commit, minimal) - -`resin-core` keeps `--config `. New behavior: - -- If `--config ` is passed and the file exists, parse it as JSON into - `CoreConfig` (with `sanitize_lanes` clamp). Surface a tracing::info - line listing the values actually used. Failures (file missing, bad JSON, - out-of-range lanes) do NOT panic — they log a warning and fall back to - `CoreConfig::default()`. Never crash the CI backend target over a - malformed config input. -- Emit a startup banner that explicitly states: "resin-core is a stub - binary; the desktop Tauri shell owns the live gateway lifecycle. Full GUI<->CLI parity is tracked in ADR-0009 + ADR-0001". -- Do NOT bind axum, do NOT spawn a sidecar, do NOT touch the network. - These capabilities are the VPS-phase decision. - -## Consequences - -- The CI backend build target still compiles and now logs an honest - banner. Nightly audits (code-review / security-review / neat-freak) - will scan the binary surface and confirm no network listener was - added. -- The next agent doing the VPS-parity refactor reads ADR-0009 first, - finds the three pre-vetted wheels, and does NOT re-research. The - refactor's blast radius is documented (33 IPC handlers + boot_resin - AppHandle coupling). -- AGENTS §5 close-loop still applies: a code commit changes the CLI - binary, so the release exe is rebuilt + smoke-launched. The CLI - binary's own behavior (no network, exits Ok) is verified by running - `target//release/resin-core.exe --help` and a single - `--config ` smoke that asserts the banner + fallback path. diff --git a/docs/adr/0019-build-all-ps1-and-release-standardization.md b/docs/adr/0019-build-all-ps1-and-release-standardization.md deleted file mode 100644 index 974ad6c0..00000000 --- a/docs/adr/0019-build-all-ps1-and-release-standardization.md +++ /dev/null @@ -1,37 +0,0 @@ -# ADR-0019: build-all.ps1 + release standardization - -Date: 2026-08-06 -Status: ACCEPTED -Context: Only build-all.sh exists (Git Bash only). No PowerShell equivalent. Version hardcoded in tauri.conf.json. No SHA256 checksum on artifacts. - -## Decision - -Create scripts/build-all.ps1 as a PowerShell-native equivalent of -build-all.sh so the developer can run a full build-stage pipeline without -Git Bash. The .ps1 script: -1. Reads RESIN_UPSTREAM_MANIFEST.yaml for sidecar version (ADR-0017) -2. Runs tsc + vite build (frontend) -3. Runs cargo build --release --features custom-protocol (backend + GUI) -4. Stages portable exe + sidecar to release/windows-gui/ -5. Computes SHA256 of each staged artifact -> release/sha256.txt -6. Smoke-launches the staged portable exe (title + pid alive check) - -Linux/macOS GUI builds remain a CI matrix job (cross-compiling Tauri + -Go sidecar on Windows is not practical). The local build is the Windows -guaranteed artifact; CI produces the cross-platform artifacts. - -backend headless tarball: cargo build --release -p resin-core produces -the library crate; a standalone headless binary is not yet wired. If -needed later, add a bin target in resin-core Cargo.toml + a stage step. - -## Alternatives rejected - -- (B) maintain only build-all.sh: requires Git Bash, friction for PS-only devs. -- (C) full local cross-compile: Tauri needs per-target rustc + native deps; - Go binary needs per-OS build. Not practical on a single Windows host. - -## Tradeoffs - -- +80 lines new PowerShell script -- Developer can build + stage entirely in PowerShell -- Linux/macOS artifacts still need CI (manual workflow_dispatch) diff --git a/docs/adr/0020-test-per-capability-closed-loop.md b/docs/adr/0020-test-per-capability-closed-loop.md deleted file mode 100644 index f2cd7677..00000000 --- a/docs/adr/0020-test-per-capability-closed-loop.md +++ /dev/null @@ -1,37 +0,0 @@ -# ADR-0020: Test-per-capability closed loop - -Date: 2026-08-06 -Status: ACCEPTED -Context: AGENTS sec4 requires per-behavior tests. Q7 grill establishes the test protocol for the T2 lifecycle/pipeline work. - -## Decision - -Each new capability from ADR-0016/0017/0018/0019 ships with a closed-loop -test in the SAME commit: - -- RingBuffer (ADR-0016): #[test] ring_buffer_evicts_oldest + bound + drain -- Crash restart (ADR-0016): mock child death -> verify retry count + backoff -- Two-phase shutdown (ADR-0016): mock SIGTERM -> wait -> SIGKILL sequence -- stdout/stderr pipe drain (ADR-0016): CommandEvent -> ring buffer assertion -- Manifest YAML parse (ADR-0017): read RESIN_UPSTREAM_MANIFEST.yaml fields -- fetch_resin version from manifest (ADR-0017): PS1 + sh read YAML not hardcoded -- Read-only ResinClient retry (ADR-0018): mockito 503 -> retry -> 200 happy path -- build-all.ps1 stage + SHA256 (ADR-0019): staged exe exists + sha256 matches - -No capability commit is complete without its test. This is the internal -review mechanism promised in Q5. - -## Rationale - -Ponytail: non-trivial logic (branch, loop, parser, security path) leaves -ONE runnable check. Each capability here has a branch or loop that can -silently break. The test is the smallest thing that fails if the logic -decays. - -Writing tests at the end risks forgetting edge-case behavior. Writing -them inline forces the implementation to be testable from the start. - -## Tradeoffs - -- Slightly slower per-commit (write impl + test together) -- Every diff has evidence; no test debt accumulation diff --git a/docs/adr/0023-node-pool-tree-view.md b/docs/adr/0023-node-pool-tree-view.md deleted file mode 100644 index 9aad9664..00000000 --- a/docs/adr/0023-node-pool-tree-view.md +++ /dev/null @@ -1,21 +0,0 @@ -# ADR-0023: Node Pool Collapsible Tree View - -Status: ACCEPTED -**Date**: 2026-08-09 - -## Context - -NodesView.tsx displays flat table, no subscription grouping, no latency. - -## Decision - -Adopt clash-verge-dev ProxyGroup pattern: -- Level 1: Subscription (collapsible), sub name + node count + health rate -- Level 2: Node rows with display_tag / region / latency(ms) / health -- Latency color: <200ms green, 200-500ms yellow, >500ms red, timeout gray -- Click row to expand details (egress_ip, failure_count, last_probe_time) -- Search box filters across subscriptions -- 10s auto-refresh (existing) - -## Scope -Pure frontend (~150 JSX + tests). NodeItem needs reference_latency_ms added. \ No newline at end of file diff --git a/docs/adr/0025-topology-dynamic-pool-edges.md b/docs/adr/0025-topology-dynamic-pool-edges.md deleted file mode 100644 index d3629027..00000000 --- a/docs/adr/0025-topology-dynamic-pool-edges.md +++ /dev/null @@ -1,28 +0,0 @@ -# ADR-0025: Topology Canvas C-Column Dynamic Pool + Strategy-Labeled Edges - -Status: ACCEPTED -**Date**: 2026-08-09 - -## Context - -C-column groups by region. After ADR-0022 strategy engine, platforms select -nodes via multiple A-class strategies, not just region. Region grouping no -longer represents real B->C relationship. - -## Decision - -### C-column: subscription-folded node pool (aligned with ADR-0023) -- Nodes grouped by subscription source (collapsible) -- Each node: display_tag + latency color + health status - -### B->C edges: strategy-driven -- Edge drawn when platform A-class strategy selects a node -- Edge label: "manual", "region:US", "quality>75" -- Multiple edges = multiple strategies coexist -- Drag B->C to node = add to platform A-class manual list -- Auto-strategy edges non-deletable - -### B-column: show B-class strategy name on platform nodes - -## Scope -~200 lines TopologyView refactor + tests. Depends on ADR-0022 + ADR-0023. \ No newline at end of file diff --git a/docs/adr/0029-platform-page-ux-fix.md b/docs/adr/0029-platform-page-ux-fix.md deleted file mode 100644 index e154e788..00000000 --- a/docs/adr/0029-platform-page-ux-fix.md +++ /dev/null @@ -1,29 +0,0 @@ -# ADR-0029: Platform Page UX Fix — Drag, Strategy Display, Unbound Ports - -Status: ACCEPTED (2026-08-13) - -## Context - -Five bugs on PlatformsView.tsx identified during user testing: -1. Dragging a no-auth port to a platform silently flips auth_required to true (bindPortToPlatform calls ipcPortUpsert without auth_required, Rust defaults to true) -2. Drag feedback is poor (only opacity-60 + ring-2) and triggers text selection (no preventDefault) -3. A-class strategy hidden behind chevron expand, not visible on card -4. A-class selectors (manual/region/subscription) are plain text boxes, no pickers -5. New ports auto-assigned to Default platform (hardcoded fallback) - -PWM research (GPT-5.6 Terra, conversation ce3f718d) confirms mature proxy GUIs use: chip-based multi-select, inline badges, 3-layer drag feedback (preventDefault + userSelect:none + ghost/ring/scale). - -## Decision - -1. New IPC port_bind_platform(port, platform_name) that only PATCHes the Resin endpoint platform binding, never touches auth_required. bindPortToPlatform TS calls this instead of ipcPortUpsert. -2. Drag onPointerDown: e.preventDefault() + document.body.style.userSelect=none. Source card: opacity-50 + cursor-grabbing. Target card: ring-2 ring-offset-2 scale-[1.02] transition. -3. A-class strategy shown as inline badge chips on platform card (A:manual, A:region[HK,JP], etc). Delete expandedPlatform state + chevron + strategy-inline panel. -4. A-class selectors replaced with chip-based multi-select: manual= nodeList checkbox chips, region= distinct regions toggle chips, subscription= subscriptionList toggle chips, quality= top_n input + live preview. -5. handleAddPort: platform_name = newPlatformName.trim() || empty string (unbound). port_upsert Rust accepts empty platform_name (skip port_mappings row). UI shows Unbound badge. - -## Consequences - -- port_upsert no longer the only write path; port_bind_platform is the binding-only path -- Strategy panel simplified: no expand/collapse, all info visible inline -- Chip selectors require nodeList + subscriptionList data already available via existing IPC -- Unbound ports require UI badge to communicate state to user diff --git a/docs/adr/0030-cross-platform-build-compat.md b/docs/adr/0030-cross-platform-build-compat.md deleted file mode 100644 index b74a0529..00000000 --- a/docs/adr/0030-cross-platform-build-compat.md +++ /dev/null @@ -1,27 +0,0 @@ -# ADR-0030: Cross-Platform Build/Compile/Test Compatibility - -Status: ACCEPTED -> **Date**: 2026-08-14 -> **Supersedes**: None (complements ADR-0019) - -## Context - -The project has grown from Windows-only to a three-platform release matrix. However, the local build scripts have accumulated platform-specific hardcoding: build-all.sh has no test step, flow guard only works on Windows, fetch_resin.sh uses cwd-relative paths, CI portable job matrix.bin uses lowercase but Cargo.toml bin name is uppercase, no SHA256 checksums generated locally, and verify-build.sh runs cargo test --workspace which fails on Windows host. - -## Decision - -1. Keep build-all.ps1 as-is (Q1-C). Only fix build-all.sh for cross-platform. -2. build-all.sh gains test steps (Q2-A): cargo test -p resin-core --lib + vitest run + i18n:check. -3. verify-build.sh gets CI/local split (Q5-A): CI=true -> full workspace; local -> resin-core only. -4. Three-platform flow guard (Q3-A): Windows direct, macOS open, Linux with DISPLAY shield. -5. Backend tar.gz staged locally (Q4-A): bin name from Cargo.toml, not hardcoded. -6. Unified binary naming (Q6-A + Q9-A + Q10-A): EgressAPIKEY (GUI), egressapikey-headless (headless), resin-core (backend). -7. fetch_resin.sh SCRIPT_DIR anchoring (Q7-A): repo root resolved from script location, not cwd. -8. SHA256 checksums (Q8-A): generated for all staged artifacts. - -## Consequences - -- build-all.sh becomes the single cross-platform local build entry point (bash). -- build-all.ps1 remains Windows-only convenience but no longer the canonical path. -- CI portable job matrix.bin corrected to match Cargo.toml bin names. -- verify-build.sh no longer fails on Windows dev host. diff --git a/docs/adr/0031-platform-gui-refactor.md b/docs/adr/0031-platform-gui-refactor.md deleted file mode 100644 index 033ac4d9..00000000 --- a/docs/adr/0031-platform-gui-refactor.md +++ /dev/null @@ -1,40 +0,0 @@ -# ADR-0031: Platform GUI Refactor — Split Card, Chip Selectors, Smart Port, Thin Scrollbar - -Status: ACCEPTED (2026-08-14) - -## Context -ADR-0029 (T8) planned chip-based multi-select for A-class strategy selectors but the implementation left text inputs in place. The platform card A/B strategy areas are visually asymmetric (B-class `` B-class and old A-class text panel. - -2. **Chip-based A-class multi-select (Q2=A)**: manual=checkbox chips from ipcNodeList, region=toggle chips from distinct regions in nodeList, subscription=toggle chips from ipcSubscriptionList, quality=top_n input + live Top-3 preview sorted by node latency. Replace all 4 text-input selectors. - -3. **Port auth persistence (Q3=A)**: New loadPortAuthDefault/savePortAuthDefault in settings.ts using tauri-plugin-store settings.json#portAuthDefault. newAuthRequired initialized from load on mount, saved on toggle change. Same persistence level as splitRatio. - -4. **Thin scrollbar CSS (Q4=A)**: 6 lines in src/styles.css: `scrollbar-width:thin` + `scrollbar-color` + `::-webkit-scrollbar{width:6px,height:6px}` + thumb + dark variant. Zero new deps. Covers Firefox (scrollbar-width) + WebView2/Chromium (::-webkit-scrollbar). - -5. **Remove B-class text summary (Q5=A)**: Delete L434 summary line (strategyToI18nKey(p.allocationPolicy) + leases + routableNodes). Move leases.length + routableNodeCount to right-top pill badge alongside delete button. B-class name only appears in lower chip UI. - -6. **Smart port suggestion (Q6=A)**: New #[tauri::command] port_suggest(db) in mod.rs — read port_mappings used ports, from 17990 increment + skip + TcpListener::bind probe, return first available u16. New ipcPortSuggest in ipc.ts. Frontend useEffect on mount auto-fills newPort. - -## Consequences - -- Platform card grows taller (A/B split uses more vertical space) but information is cleaner and more discoverable. -- ipcNodeList return type needs a TS interface (NodeEntry) — currently `Promise`. Add interface in ipc.ts, no Rust change. -- settings.ts gains 2 new functions (loadPortAuthDefault/savePortAuthDefault) following existing loadSplitRatio/saveSplitRatio pattern. -- styles.css gains 6 lines of global CSS — affects all scrollable areas in the app (App.tsx main, PlatformsView lists, NodesView, DiagnosticsView). -- port_suggest IPC adds 1 command to mod.rs (55 → 56 commands). Needs validator guard. -- i18n: 9 new keys across all 18 locales. -- Supersedes ADR-0029 T8-6 (chip selectors) which was planned but never implemented. - -## Ponytail assessment - -- No new dependencies (ToggleGroup is shadcn/Radix pattern re-implemented with Tailwind classes, matching existing badge chip pattern already in PlatformsView). -- Reuses existing ipcNodeList + ipcSubscriptionList IPC (already shipped, just not imported in this view). -- Reuses existing loadSplitRatio/saveSplitRatio patterns for new settings persistence. -- Reuses existing sidecar.rs pick_free_loopback_port logic concept for port_suggest. -- Total estimated: ~200 lines TS refactor + ~25 lines Rust + ~6 lines CSS + i18n. diff --git a/docs/adr/0032-platform-page-ux-polish.md b/docs/adr/0032-platform-page-ux-polish.md deleted file mode 100644 index 8fc8383e..00000000 --- a/docs/adr/0032-platform-page-ux-polish.md +++ /dev/null @@ -1,68 +0,0 @@ -# ADR-0032: Platform Page UX Polish - -**Date**: 2026-08-14 -Status: ACCEPTED -**Supersedes**: ADR-0029 (platform page UX fix, T8) partially — adds collapsible cards, per-platform sync, port form polish - -## Context - -The platform page (PlatformsView.tsx) has accumulated UX issues across T8-T10 iterations: -1. Platform cards always render at full expansion — no way to scan many platforms quickly -2. Strategy chip selection has weak visual feedback (only color change) -3. Manual node selection shows all nodes with no search/filter — unusable with 50+ nodes -4. Global Apply button causes "partial not found" errors when strategyConfig contains stale deleted platforms -5. Port form flashes hardcoded defaults ("17990", "Default", true) before persisted values load -6. Port cards cannot be selected for keyboard operations -7. Port form uses 2-column grid wasting vertical space -8. Port cards always expanded showing all details - -## Decision - -### Q1 — Collapsible platform cards (shadcn Collapsible pattern) - -**Collapsed state**: single summary row — platform name + A/B strategy badges (`A:manual[3]` or `B:latency`) + leases/nodes pill + chevron icon. -**Expanded state**: full A/B split chip selectors with search for manual mode. -Click the chevron or card header to toggle. - -### Q2 — Ring feedback on selected chips (B) - -Add `ring-2 ring-primary ring-offset-1` in addition to the existing color change. Provides clear visual boundary around selected state. - -### Q3 — Manual node search with hybrid selected-first (A+hybrid) - -Inline search input above the node chip list. Typing filters chips by display_tag prefix match. Selected nodes that match the search stay in their original position; selected nodes that DO NOT match the search filter are pulled to a "Selected (N)" section at the top so they remain visible and removable regardless of the filter. - -### Q4 — Per-platform sync mode (C) - -**Remove** the global Apply button. Each chip change immediately: -1. Updates local `strategyConfig` state -2. PUTs the updated config to `egressapikey-strategy.json` -3. PATCHes the specific platform's `region_filters` on the live Resin sidecar - -This eliminates the stale-platform-batch problem. Additionally, auto-clean: before any strategy operation, filter `strategyConfig.platforms` to only those that exist in the live Resin `platform_list_full()` result. - -**Fix `a_class_regions` Manual bug**: The `Manual` case in `strategy_engine.rs` must map `manual_nodes` (node hashes) to region codes by looking up each hash in the healthy nodes list and collecting the unique regions. The GUI writes `manual_nodes`, NOT `regions` — the current code returns `strategy.regions` which is empty for Manual mode. - -### Q5 — Port form bootstrap gate (A) - -Add `bootstrapped: boolean` state, initialized to `false`. Render a skeleton/div placeholder while `false`. The first `useEffect` loads persisted port form defaults (port, protocol, label, platform, auth) from settings.json, sets all state to the loaded values, then flips `bootstrapped=true`. First paint after bootstrap shows the real persisted values — no flash. - -### Q6 — Port card toggle select (A) - -`~~selectedPort~~ (REMOVED in T13): number | null` state. Click a port card toggles selection. Selected card shows `ring-2 ring-primary`. When a port is selected, pressing Delete/Backspace triggers `handleDeletePort`. Clicking the same card again deselects. - -### Q7 — Port form compact inline (A) - -Replace the 2-column grid with a single flex row: `w-20` (port) + `w-24` (protocol select) + `flex-1` (label input) + `shrink-0` (auth checkbox) + `shrink-0` (add button). One line, no wrapping. - -### Q8 — Port card collapsible (A) - -Matches Q1 pattern. Collapsed: single row — `port/proto` + health dot + auth icon + delete button + chevron. Expanded: auth credentials + health details + platform binding. Click row to toggle. - -## Consequences - -- **Positive**: cleaner UX, no stale-platform errors, no default flash, better space utilization, keyboard-friendly port management -- **Negative**: more state management in PlatformsView (bootstrapped, ~~selectedPort~~ (REMOVED in T13), expanded card sets) — mitigated by keeping each concern in its own useState -- **Risk**: per-platform sync means more PATCH calls to Resin — but each is a single platform PATCH, lighter than the old batch apply - -## Status: ACCEPTED diff --git a/docs/adr/0033-platform-page-ux-fix-t12.md b/docs/adr/0033-platform-page-ux-fix-t12.md deleted file mode 100644 index 4f9907c3..00000000 --- a/docs/adr/0033-platform-page-ux-fix-t12.md +++ /dev/null @@ -1,25 +0,0 @@ -# ADR-0033: Platform Page UX Fix (T12 Fix Branch) - -**Date**: 2026-08-14 -Status: ACCEPTED -**Supersedes**: ADR-0032 partially (adds context-menu disable, hover expand, subscription-folded manual list, label compact) - -## Context - -T11 (ADR-0032) shipped collapsible cards, chip selectors, per-platform sync, port compact form, bootstrap gate. User testing surfaced 5 remaining issues: port selected gray conflict, WebView2 context menu, no hover expand on port cards, ugly "Selected (N)" in Manual mode, label input too wide squeezing out auth/add controls. - -## Decision - -1. **Q1 — No fix**: drag opacity-50 is transient (pointerdown to pointerup); selected ring-2 ring-primary ring-offset-1 is persistent after click. No real conflict. -2. **Q2 — CSS + config double guard**: App root
onContextMenu={(e) => e.preventDefault()} + body CSS user-select:none (exempt input/textarea/select) + tauri.conf.json if Tauri 2 supports it. -3. **Q3 — hoveredPort state**: onMouseEnter/onMouseLeave drive a transient hoveredPort state. isExpanded = expandedPortCards.has OR hoveredPort. Manual chevron expand persists in expandedPortCards. -4. **Q4 — Reuse NodesView groupBySub**: add tags field to NodeEntry, inline subName + groupBySub logic, replace flat Selected(N) + unselected list with subscription-folded collapsible groups. -5. **Q5 — min-w-0 max-w-[200px] flex-1**: label input has elastic width with upper bound so auth checkbox and add button survive narrow middle pane. - -## Consequences - -- **Positive**: WebView2 native context menu fully suppressed; hover expand improves discoverability; subscription-folded manual list aligns with NodesView mental model; label compact prevents layout overlap -- **Negative**: hoveredPort state adds one more useState; groupBySub logic duplicated inline (NodesView already has it, but import across views would be over-engineering) -- **Risk**: hover expand + pointer-drag drag start on same
  • — onMouseEnter fires before onPointerDown, so hover expand happens first, then drag starts; should not conflict - -## Status: ACCEPTED diff --git a/docs/adr/0034-canvas-v2-dagre-strategy-layout.md b/docs/adr/0034-canvas-v2-dagre-strategy-layout.md deleted file mode 100644 index ac5a325e..00000000 --- a/docs/adr/0034-canvas-v2-dagre-strategy-layout.md +++ /dev/null @@ -1,36 +0,0 @@ -# ADR-0034: Canvas V2 — dagre + strategy-driven layout + zustand cache - -Status: ACCEPTED -Date: 2026-08-14 - -## Context - -T9 (commit c45334c) introduced subscription-folded C column + strategy-labeled edges -+ dual A+B badges + full-area Handle. User testing surfaced 4 bugs: -1. C column displays all nodes flat; no strategy-driven auto-layout -2. Canvas laggy + edge anchors illogical (hardcoded positions) -3. Empty-state messages flash on entry before sync() resolves -4. No caching; every view entry triggers 4 IPC calls + full re-render - -## Decision - -1. C column: strategy-driven filter — only show nodes targeted by at least one - platform's region_filters or manual selection. Unselected nodes collapse as - "+N unbound". (Kiali Service Graph pattern) -2. Layout: dagre auto-layout (official @xyflow/react recommendation) replaces - hardcoded x:0/300/640 positions. rankdir=LR, ranksep=80, nodesep=40. -3. Handle: revert from full-area overlay to fixed edge midpoint (right/left center), - connectionRadius=40 for snap zone. Full-area Handle was eating mousedown. -4. Flash fix: move 3 empty-state messages into the opacity-0/100 gate div; - setReady(true) after sync() resolves, not just onInit. -5. Cache: new topologyStore (zustand create + shallow compare) holds - platforms/subGroups/leases/ports. sync() writes via store setState; - components subscribe via useStore(selector, shallow). 5s poll restored - — shallow skip prevents re-render when data unchanged. - -## Consequences - -- New dep: dagre + @types/dagre (~60KB) -- T9's full-area Handle (ADR-0025 Q2) superseded by fixed-position Handle -- TopologyView refactored from local useState to zustand store selectors -- 5s poll + visibilitychange re-added (removed in T9 rewrite, now safe with shallow) diff --git a/docs/adr/0038-t15-perf-fixes.md b/docs/adr/0038-t15-perf-fixes.md deleted file mode 100644 index bf2a08da..00000000 --- a/docs/adr/0038-t15-perf-fixes.md +++ /dev/null @@ -1,30 +0,0 @@ -# ADR-0038: T15 Performance Fixes — DiagnosticsView usePoll, log-level control, canvas memo+throttle, reqwest::Client reuse - -Status: ACCEPTED -**Date**: 2026-08-15 -**Relates to**: GRILL_T15_PERFORMANCE_FIX.md, ADR-0037 (prior session) - -## Context - -Post-T14 performance audit surfaced 4 remaining gaps: -1. DiagnosticsView used raw `setInterval` for its 5s poll — not aligned with the T14-3 `usePoll` hook used everywhere else. -2. No runtime log-level control — users debugging a specific subsystem had to read the full INFO-level log file. -3. Canvas custom node components (EntryPortNode, PlatformNode, SubscriptionGroupNode, RegionGroupNode) re-rendered on every parent state change even when props were unchanged. -4. `ResinClient::new()` constructed a fresh `reqwest::Client` per IPC invocation, rebuilding the TLS pool + connection cache each time. - -## Decisions - -- **T15-1 (Q7)**: DiagnosticsView replaced `setInterval` with the existing `usePoll` hook from T14-3. The interval is now uniform; 1 vitest closed-loop pins the hook is used (no raw `setInterval` call). -- **T15-2 (Q8)**: New `set_log_level` IPC command `src-tauri/src/commands/mod.rs` accepts `error|warn|info|debug` and swaps the `tracing` filter via an atomic gate. The `tauri-plugin-tracing` reload handle is not exposed publicly, so the fallback is an atomic `LogLevel` gate that the IPC layer reads per-call — a Ponytail co-protection, not a global subscriber rebuild. 2 cargo tests (valid level accepted, invalid rejected) + 1 vitest (TS wrapper forwards correct shape). i18n keys added to all 18 locales. -- **T15-3 (Q9 A+B)**: The four custom node components (`EntryPortNode`, `PlatformNode`, `SubscriptionGroupNode`, `RegionGroupNode`) are wrapped in `React.memo`. The canvas has only `onMoveEnd` (persists the viewport once drag finishes); there is no per-frame `setViewport` from a drag handler, so no throttling is needed — the single `requestAnimationFrame` at line 561 guards only the one-shot `fitView` reset, keeping the main-thread idle during drag. 2 vitest closed-loop pin memo prevents re-render with same prop reference + the module loads (components non-null). -- **T15-4 (Q5)**: `crates/resin-core/src/resin_client.rs` gained a static `CLIENT: OnceLock` + `fn shared_client()`. `ResinClient::new()` now clones the shared client handle instead of constructing one per call. Eager-pool init happens once at first use. 1 cargo test verifies pointer-identity of two `shared_client()` calls. - -## Consequences - -- No new crate, no new runtime dependency. ADR-0037 stays in force for the route_id decision (T16) and is unchanged by T15. -- The atomic log-level gate is co-protection: the underlying `tauri-plugin-tracing` file appender still emits at its configured level on its own worker thread; the gate is the IPC surface the user sets from GUI. The two are coordinated at the GUI command boundary. -- Canvas `React.memo` is safe because all four custom node props are reference-stable Zustand-derived values — no inline object props are passed that would defeat memoisation. The four component names are `EntryPortNode` (entry proxy port), `PlatformNode` (each platform), `SubscriptionGroupNode` (collapsible per-subscription group), `RegionGroupNode` (one per region). - -## Risk - -- Low across all four fixes. OnceLock is std-stable; `React.memo` is non-breaking; `usePoll` is already proven from T14-3; the atomic log-level gate does not touch the subscriber. diff --git a/docs/adr/0039-canvas-v3-fold-center-strategy-sync.md b/docs/adr/0039-canvas-v3-fold-center-strategy-sync.md deleted file mode 100644 index b1b6f82c..00000000 --- a/docs/adr/0039-canvas-v3-fold-center-strategy-sync.md +++ /dev/null @@ -1,52 +0,0 @@ -# ADR-0039: Canvas v3 - fold consistency, dagre center correction, strategy badge sync, two-file whitebox boundary - -Status: ACCEPTED (T15-v3 grill, 2026-08-16) -Supersedes parts of: ADR-0034 (canvas v2 dagre-strategy-layout) - S4 corrects the center-offset formula introduced there; -ADR-0036 (strategy single-source-of-truth) - S2/S3 extends the sync contract to *reading* strategy fields, not just writing. - -## S1 Two-file whitebox boundary - -Keep config_export/config_import and egressapikey-strategy.json as two separate files. -The first contains Resin-native Platform + Subscription fields (regex_filters, region_filters, allocation_policy, sticky_ttl, subscription url). -The second contains shell-layer strategy identity (a_class, b_class, manual_nodes, subscriptions, top_n). -Settings surfaces BOTH paths with reload buttons; no in-app text editor (Ponytail: users who whitebox-edit use an external editor; a JSON textarea adds validation surface without removing the risk). -Rejected: merging into one JSON. The backend vs strategy-layer boundary is the whole point of the split; merging hides it. - -## S2 Canvas reads strategy fields (ADR-0036 read-side extension) - -ADR-0036 pinned the *write* pipeline (drag -> strategyConfig JSON -> apply -> Resin PATCH). The *read* side was left implicit and rotted: -TopologyView computed aClassLabel from p.region_filters?.length alone - i.e. only the Region variant was ever displayed; Manual / Quality / Subscription all rendered as Manual. -Fix: PlatformFull gains aClass, bClass, manualNodes, subscriptionNames, topN; sync() merges these from strategyConfig the same way it already merges regions. -aClassLabel and bClassLabel are then computed from the merged fields via strategyToI18nKey-equivalent mappings, not inferred from region_filters. -Closed loop: change a_class/b_class in the JSON -> reload -> canvas badge updates. Change in PlatformsView -> save -> same path. - -## S3 Region viewMode fold consistency - -The region/subscription toggle is preserved (Q1 = A) because the two viewModes serve different tasks: subscription mode traces which subscription feeds which nodes, region mode traces which geographic areas the Platform covers. -Current bug: region mode auto-expands every region group into per-node rows, flooding the canvas with hundreds of cards and causing the lag + clutter symptom. -Fix: RegionGroupNode keeps the same fold contract as SubscriptionGroupNode - collapsed summary (region label + healthy/total count), expandable on click only. No default expansion. Auto-expand when there is only one region is rejected: it breaks the stable-layout guarantee. - -## S4 dagre center correction - -Remove the offsetX = graphLabel.width / 2 line in layoutNodesViaDagre (T15-4 introduced it as a center to origin hack). -The authoritative formula per kimi_k26 research (cross-checked against the official ReactFlow + dagre example) is: -position = { x: pos.x - nodeWidth/2, y: pos.y - nodeHeight/2 } - pure center-to-top-left conversion. -dagre internal marginx/marginy: 20 already reserves canvas breathing room; no extra graph-width subtraction. -Effect: setViewport({x:0,y:0,zoom:1}) now shows the graph top-left margin corner (~ origin), and fitView({maxZoom:1}) still auto-frames on init. -The previous drift (more nodes = wider graph = bigger offset = visible shift away from Home) disappears. - -## S5 Settings whitebox reload surfaces - -Settings already has the network-whitebox reload + path display. Add the same pattern for the strategy JSON: -- show egressapikey-strategy.json absolute path (read-only label) -- Reload from disk button that re-reads the file into the in-memory state and calls strategy_apply so the GUI reflects external edits -- no text editor widget, no save button on this surface (edits go through external editor or through PlatformsView GUI) -This makes the whitebox-everything-in-files mental model visible from inside the GUI without embedding a code editor. - -## Consequences - -- ~40 lines changed in TopologyView.tsx (PlatformFull widening + sync merge extension + aClass/bClass label computation + dagre offset deletion) -- ~15 lines in RegionGroupNode (fold contract alignment) -- ~10 lines in SettingsView.tsx (strategy JSON path + reload, mirroring the existing network-whitebox card) -- New i18n keys for A-class label variants (topology.aClassManual / topology.aClassQuality / topology.aClassSubscription) across all 18 locales -- Tests: existing T15-3/T15-5 plus new assertions that a non-region a_class renders the correct badge, and that Home centers the graph regardless of node count. diff --git a/docs/adr/0040-canvas-region-filter-port-bind-minimap.md b/docs/adr/0040-canvas-region-filter-port-bind-minimap.md deleted file mode 100644 index 4aa38bbf..00000000 --- a/docs/adr/0040-canvas-region-filter-port-bind-minimap.md +++ /dev/null @@ -1,70 +0,0 @@ -# ADR-0040: Canvas region filter + port drag-bind + MiniMap i18n - -Status: ACCEPTED -> Date: 2026-08-16 -> Depends on: ADR-0039 (canvas v3 fold + strategy sync) - -## Context - -Three bugs surfaced after T15-v3 canvas commit (db257ad): - -1. Region viewMode generates region group cards for ALL regions that have nodes, - regardless of whether any platform selected them via region_filters. This floods - the canvas with hundreds of unbound region cards and causes lag. - -2. Canvas drag from entry-port to platform does nothing. onConnect only handles - platform -> subgroup/regiongroup connections. Port-to-platform binding exists as - an IPC command (port_bind_platform) but is not wired into canvas drag. - -3. MiniMap component has no i18n tooltip; hover over the minimap area shows no - localized guidance. MiniMap also uses default grey nodeColor, making nodes - indistinguishable in the overview. - -## S1 Region viewMode: only show platform-bound regions - -Region group nodes are only generated when at least one platform has the region in -its region_filters. Implementation: add a selectedRegions.has(region) guard -before pushing a regiongroup node into the node list. This mirrors the existing -subscription viewMode filter at L705. - -Rejected: showing all regions with reduced opacity (Kiali idle/active pattern). -Rejected because the user explicitly chose A: "only show regions the platform -strategy actually selects." - -## S2 Port drag-bind: wire entry-port -> platform into onConnect - -Add a new branch in onConnect: when conn.source.startsWith("entry-port-") and -conn.target.startsWith("platform-"), extract the port number from the source -id and the platform name from the target id, then call ipcPortBindPlatform(port, -platformName) followed by sync(). - -This reuses the existing Rust port_bind_platform IPC command (L1951 in -commands/mod.rs) and the TS wrapper ipcPortBindPlatform (L425 in ipc.ts). No -new IPC needed. - -Rejected: auto-creating edges without IPC. The port-platform binding must persist -to SQLite port_mappings; without the IPC call the edge would vanish on next sync. - -## S3 MiniMap: i18n tooltip + nodeColor customization - -Wrap in a
    to provide a -localized hover tooltip. Add -odeColor function that colors nodes by type: -entryPort = blue, platform = purple, subscriptionGroup = green, regionGroup = -amber. Add maskColor for a dark viewport overlay. No new dependency; uses -ReactFlow 12 built-in MiniMap props. - -Rejected: replacing MiniMap with a custom overview panel. Ponytail: MiniMap -already works; custom replacement adds ~200 lines for zero new capability. - -## Consequences - -- TopologyView.tsx: ~15 lines (region filter guard + onConnect port branch + - MiniMap wrapper + nodeColor function) -- onConnect: new entry-port -> platform branch (~10 lines) -- MiniMap: wrap in div + nodeColor prop (~5 lines) -- New i18n keys: topology.minimapHint x18 locales -- Canvas entry: "open strategy config" button (~10 lines, reuses ipcGetConfigDir + - openPath) -- Tests: 3 new vitest assertions (region filter guard, port drag-bind forward, - MiniMap tooltip renders i18n text) \ No newline at end of file diff --git a/docs/adr/0041-canvas-v4-node-pool-toolbars-merge.md b/docs/adr/0041-canvas-v4-node-pool-toolbars-merge.md deleted file mode 100644 index cf697f85..00000000 --- a/docs/adr/0041-canvas-v4-node-pool-toolbars-merge.md +++ /dev/null @@ -1,117 +0,0 @@ -# ADR-0041: Canvas v4 node pool + single aggregation column + MiniMap ariaLabel + toolbar merge - -Date: 2026-08-17 -Status: ACCEPTED -Supersedes: T14-4 region viewMode dual-list pattern, T16-3 MiniMap div[title] wrapper, T16-4 standalone openStrategyConfig button position - -## Context - -Four bugs surfaced after T16 commit (745837e): - -1. **Entry-port column stray nodes**: in `viewMode === "region"` the rawNodes builder keeps - the `subGroups.forEach((g) => { ... list.push({ type: "subscriptionGroup" ... }) }`` loop - (L697-731) running unconditionally. Because edges in region viewMode are - `platform-* -> regiongroup-*` NOT `platform-* -> subgroup-*`, the subGroup nodes - have no incoming/outgoing edges. dagre's `rankdir: "LR"` layout places edgeless nodes - into upper-left free space (nearest to the entry-port column due to rank balance), - so the user sees a pile of "subscription" cards under or beside the entry-port - column. This is the visual "端口下面莫名多出一堆节点的卡片" the user reported. - T16-1's `if (!selectedRegions.has(region)) continue` guard only filters - regionGroup pushes, not subGroup pushes — the root cause is the unconditional - subGroup loop. - -2. **openStrategyConfig button overlaps**: T16-4 placed the button at - `absolute bottom-20 left-4 z-10` directly between CanvasControls (bottom-2 left-2) - and the MiniMap (bottom-0 right-0). Three elements stack in one corner; button - overlaps CanvasControls hover/click area on narrow viewports. - -3. **MiniMap tooltip not i18n-realized**: T16-3 wrapped `` in - `
    `. The wrapper is a static block whose - only child is absolutely positioned; the wrapper therefore has height 0 and its - native `title` tooltip never fires a hit-test. Source verification on - @xyflow/react 12.11.2 dist shows `` already renders - `{ariaLabel}` when given - the `ariaLabel` prop; this SVG `` provides both the accessible name and a - native hover tooltip in Chromium / WebView2. The div wrapper is dead weight. - -4. **Node row duplication across subscriptions**: in region viewMode, the - `for (const g of subGroups) for (const n of g.nodes) { entry.nodeRows.push(...) }` - loop copies every node into the region bucket without dedup-by-node_hash. If the - same node_hash appears under subscription A and subscription B (Resin deduplicates - the upstream but not across different subscription names that reference the same - proxy), the user sees identical rows twice inside one regionGroup. Kiali's - canonical "group by X / group by Y" mental model has a single node instance per - underlying entity; viewMode only changes the grouping dimension, not cardinality. - -## Decision - -### S1 — Single aggregation column + viewMode guard on subGroup - -The subGroup push block (L697-731) MUST be gated by `if (viewMode !== "subscription") return;` -early in its body. Symmetrically, the regionGroup push block (L735-767) is already gated -by `if (viewMode === "region")` — leave it. Result: in any viewMode, exactly one C-column -node type (subscriptionGroup OR regionGroup) is constructed. No edgeless nodes leak into -dagre's free-space fallback. - -Kiali reference: "group by app / group by version" preserves one node-per-entity invariant -by construction; the viewMode dimension is the only thing that changes. Same invariant here. - -### S2 — Cross-subscription node dedup by node_hash - -Both the region viewMode route (L744-751 for loop) and the subscription viewMode route -(L718-726 nodeRows map) MUST dedup source nodes by a stable identity key BEFORE building -the display rows. Resin v1.2.0 `/api/v1/nodes` returns a `node_hash` field per node; -we use it as the dedup key. When iterating `for (const g of subGroups) for (const n of -g.nodes)` in region viewMode, maintain a `Set<string> seen = new Set()` and skip when -`seen.has(n.node_hash)`. Similarly for subscription viewMode inside one subscription -group: dedup by node_hash within that group (this was previously implicit because one -subscription rarely echoes nodes; we now guard it explicitly). - -### S3 — MiniMap ariaLabel replaces div[title] - -Replace the ``<div title={...}> <MiniMap ... /> </div>` footer (L955-972) with -`<MiniMap ariaLabel={t("topology.minimapHint")} pannable zoomable nodeColor={...} maskColor={...} />`. -Drop the wrapper div entirely. `minimapHint` key stays across 18 locales. The ariaLabel -prop is the official xyflow 12 surface (verified by reading dist + intent through issue -#2858 where a maintainer documents setting `ariaLabel={null}` removes the default -"Mini Map" tooltip — proving the default IS surfaced as a native tooltip). - -### S4 — openStrategyConfig merged into CanvasControls toolbar - -Move the openStrategyConfig onClick block from the standalone `<button>` at L942-954 into -the CanvasControls component at L478-509 as a third row / a new button next to the existing -viewMode segmented control + zoom + fit + lock buttons. The toolbar group becomes one -absolute-positioned component at `bottom-2 left-2`; the standalone L942-954 block is -deleted entirely. No overlap with MiniMap. - -## Rejected alternatives - -- S1-reject: keep subGroup + regionGroup both rendered in region viewMode but add - invisible dagre `rank=same` constraints to pin subGroups to C column. Rejected: it - doubles the C-column cardinality in region viewMode and requires hand-maintained - invisible-edge bookkeeping. The viewMode switch already implies which grouping the - user wants; a one-line early-return guard is strictly simpler. - -- S2-reject: dedup by `display_tag` instead of `node_hash`. Rejected: two unrelated - proxies can share `display_tag` (e.g. both "Japan-01" from different providers); - display_tag is not a stable identity. - -- S3-reject: wrap MiniMap with Radix/Floating UI Tooltip. Rejected as the primary fix: - MiniMap's own `ariaLabel` already gives native hover tooltip with zero dependencies. - Radix/Floating would be a future enhancement only if brand-styled tooltips are needed. - -- S4-reject: standalone button moved to `top-4 right-4` with lucide FileCog icon. - Rejected: the user explicitly chose "merge into CanvasControls toolbar" (Q3 = A). Two - tool groups in the corners is the anti-pattern the user called out as "occlusion". - -## Consequences - -- TopologyView.tsx: +6 lines of guard (S1) + ~10 lines of dedup Set (S2) + delete 18 lines - of div[title] wrapper (S3) + delete 13 lines of standalone button (S4) + add ~15 lines - inside CanvasControls = net ~0 lines change. -- No new i18n keys. `topology.minimapHint` + `topology.openStrategyConfig` already exist - in 18 locales and are reused unchanged. -- No new dependencies (Radix/Floating explicitly rejected). -- Tests: +8 vitest = 225 -> 233 total. See plan for the 8 test names. -- ADR-0040 T16-3 + T16-4 partially superseded in implementation; ADR-0040 text stays as - history (per AGENTS §10 audit-trail protection). diff --git a/docs/adr/0046-node-pool-refresh-batch-progress-button-layout.md b/docs/adr/0046-node-pool-refresh-batch-progress-button-layout.md deleted file mode 100644 index 1c25e468..00000000 --- a/docs/adr/0046-node-pool-refresh-batch-progress-button-layout.md +++ /dev/null @@ -1,59 +0,0 @@ -# ADR-0046: Node Pool Refresh + Batch Progress + Button Layout - -Date: 2026-08-19 -Status: ACCEPTED -Supersedes: none (ADR-0044 S2 was already superseded by ADR-0047; this ADR layers UI/UX decisions on top of ADR-0044 S1/S3/S4 and ADR-0047) - -## Context - -The node pool tab (`src/views/NodesView.tsx`) has three user-visible bugs reported after the T20-v2 audit close (commit 1d23d74): - -1. **Refresh + BatchProbe button position is off**: the T20-v2 nested-button fix (to resolve HTML spec 4.10.1 violation) moved the two action buttons out of the title `<button>` into a sibling `<div>` below the title row; the visual result is a separate below-the-title band instead of an inline toolbar. clash-verge-rev HeadState puts all group-level IconButtons inline in the title row. -2. **Batch probe progress stuck at 0**: `handleBatchProbe` tooltip `t("nodes.batchProbing", { done: 0, total: items.length })` hardcodes `done=0`; the `done++` counter inside the chunk loop never calls a setState, so React never re-renders the tooltip, so users see `0/N` frozen even while probes resolve. -3. **Refresh sub silent + top-right refresh mental model confused**: `handleRefreshSub` sets `refreshingSub` spinner but gives no toast; `await refresh()` immediately after `ipcSubscriptionRefresh` may read stale Resin memory (though, per source-level research of Resin v1.2.0 `Scheduler.UpdateSubscription`, this is actually sync and not stale). The top-right page-level refresh button tooltip `nodes.refresh` is the same string as the per-sub refresh button, conflating two distinct actions (re-read Resin memory vs trigger Resin remote-fetch). - -## Decisions - -### S1 - Inline button row via `<div + onClick>` (Q1-A) - -Swap the outer `<button onClick={() => toggleSub(sub)}>` (L401) for `<div onClick={...} role="button" tabIndex={0}>` and bring the two IconButtons (RefreshCw + Activity) inline into the same `flex items-center gap-2` row as the chevron + name + node-count + health-rate. Title bar IS the per-group toolbar; no separate action band below. Keyboard access: add `onKeyDown` for Enter/Space. - -**Rejected**: keep nested `<button>` (already a spec violation T20-v2 fixed). **Rejected**: separate sibling div with negative margins (visual cheating, does not fix mental model). - -### S2 - Batch progress via per-probe `setProbeResults` + `batchProgress` Map (Q2-A) - -Extract `applyProbeResult(prev, hash, kind, res)` pure helper from `handleProbeNode` and call it from both the single-node path and the batch path. Each probe completion writes `setProbeResults(prev => applyProbeResult(prev, hash, "latency", res))` so every row latency updates `-` to `ms` in real-time, matching clash-verge-rev `DelayManager.setListener` per-proxy model. Add `batchProgress: Map<string, {done, total}>` state; the batch tooltip interpolates `{{done}}/{{total}}` from this Map. Increment after each probe resolves; reset on `finally`. - -**Rejected**: only display total progress text without per-row update (users cannot see WHICH node finished). **Rejected**: wait until all probes done then render once (no real-time visual feedback, same symptom as today bug). **Rejected**: full clash-rev `DelayManager` single-instance service (200+ lines of `useReducer` + listener lifecycle; overkill for our per-node need). - -### S3 - Refresh sub sync toast feedback (Q5a-A, fact-driven) - -Resin v1.2.0 `/actions/refresh` is `RefreshSubscription(id) -> Scheduler.UpdateSubscription(sub)` which for `source_type=remote` BLOCKS on `Fetcher(attemptURL)` - the new Clash YAML is already in Resin memory when the POST responds. Therefore the frontend needs no delayed/polling fallback. Toast flow: `refreshSent` fires immediately on click, `await ipcSubscriptionRefresh(name)` blocks on the sync backend, `await refresh()` re-reads now-fresh Resin memory, then compare `nodes.length` before/after to pick `refreshDone` (delta > 0) or `refreshNoChange` (delta = 0). - -**Rejected**: 1.5s / 3s delayed poll (Q2-A provisional plan - based on wrong assumption that backend is async). The source-level research of `internal/topology/subscription_scheduler.go UpdateSubscription` confirms sync-block-on-Fetcher for remote source_type. - -### S4 - Density refactor: 3 text cards to popover / footer / sticky toolbar (Q3-A + Q4-A) - -Delete the three standalone info cards (`egressPolicyNote`, `protocolWeights`, `reputationTitle`): the two policy/protocol note bodies crumb into an `<Info size=12 />` icon at the top-right corner of the giveaway StatCard with a `z-50 whitespace-pre-line` hover popover showing both notes; reputation content moves into the `lastRefresh` footer row as a 1-line compact 2-column flex (reputation summary left, timestamp right). Promote the search toolbar (search input + 5 toggle chips + expand/collapse-all) to `sticky top-0 z-30` so it stays visible while the node list scrolls. Top-right page-level refresh button tooltip renames to `nodes.syncCache` ("Re-sync backend node snapshot") to separate from the per-sub refresh semantic. - -**Rejected**: delete cards permanently (loses discoverability for new users). **Rejected**: collapse-on-expand panel (still occupies row outline space compared to popover). **Rejected**: move reputation/egressPolicyNote/protocolWeights to Settings page (i18n key migration from `nodes.*` to `settings.*` across 18 locales is scope creep). - -## Consequences - -- **Positive**: 3 user-reported bugs fixed in one surgical plan; per-row real-time latency feedback matches industry-standard (clash-verge) mental model; page-level vs group-level refresh semantic distinction becomes explicit; node pool page vertical density improves (3 dead cards to 1 popover + 1 footer line, sticky toolbar removes one scroll-to-top round-trip). -- **Negative**: the inline title row gets busier (6 elements in one flex row instead of 3+2 in two rows); at small viewports the row may wrap awkwardly. Mitigation: `flex-wrap` on the container with `gap-2` lets it wrap to second line gracefully without separate sibling div. -- **Neutral**: no new IPC commands, no new Rust surface, no backend event channel needed (T21 relies on Resin being sync - already true in v1.2.0). - -## Open debt explicitly NOT addressed - -- The double `ADR-0047` filenames (`0045-ipc-error-contract-hardening.md` and `0047-subscription-refresh-native-resin-actions.md`) - scope creep; defer to a future ADR-hygiene commit. -- `VirtualNodeList` virtualization for >50 nodes already works; clash-rev-style column reflow is bigger work and out of scope. -- `settings.json#nodeProbe` knobs surface in Settings panel (ADR-0044 S4 surface) still planned but unchanged by T21. - -## References - -- ADR-0044 (node-pool collapse + Resin-native probe + whitebox knobs) - S1/S3/S4 still hold, S2 superseded by ADR-0047. -- ADR-0047 (subscription refresh sink to Resin native `/actions/refresh` with `source_type=remote`) - T21 builds on top of this foundation. -- `docs/GRILL_T21_NODE_POOL_BUTTON_SYNC_PLAN.md` - the 5-phase execution plan implementing these decisions. -- clash-verge-rev `src/components/proxy/use-filter-sort.ts` + `proxy-item.tsx` + `use-render-list.ts` - research source for the per-proxy DelayManager listener pattern. -- Resin v1.2.0 `internal/topology/subscription_scheduler.go UpdateSubscription` - source-level evidence that `/actions/refresh` is sync-block-on-Fetcher for remote source_type. \ No newline at end of file diff --git a/docs/adr/0048-canvas-a-class-semantic-filtering.md b/docs/adr/0048-canvas-a-class-semantic-filtering.md deleted file mode 100644 index 7da4a22b..00000000 --- a/docs/adr/0048-canvas-a-class-semantic-filtering.md +++ /dev/null @@ -1,60 +0,0 @@ -# ADR-0048: Canvas C-column a_class-semantic filtering + new-platform default strategyConfig entry - -Status: ACCEPTED -> Date: 2026-08-19 -> Supersedes: T13-1 getSelectedRegions global-union filtering (L104-110), buildCColumnGroups selectedRegions param (L185-203), buildEdges region_filters-only B->C edge (L620-641) -> Builds on: ADR-0036 strategyConfig single source, ADR-0039 canvas fold, ADR-0041 canvas v4, ADR-0042 canvas pipeline sync - -## Context - -Bug 1: Canvas C-column stuck showing only FI region nodes. Root cause: buildCColumnGroups (L203) filters nodes via `selectedRegions.has(getNodeRegion(n))` where selectedRegions is the global union of all platforms' region_filters. strategyConfig merge (L784) overwrites region_filters from ps.regions, so a config with `"regions": ["fi"]` makes C-column show only FI nodes regardless of subscription switches or a_class semantics. - -Bug 2: New platforms appear below nodes instead of in B column. Root cause: buildEdges (L620-641) only draws B->C edges for platforms with non-empty region_filters. New platforms not in strategyConfig have empty region_filters -> 0 B->C edges -> dagre assigns lowest rank (bottom of graph). - -## Decision - -### S1 — C-column filtering by a_class semantics (Q1+Q3+Q4) - -buildCColumnGroups signature changes from `selectedRegions: Set<string>` to `platforms: PlatformFull[]`. Node filtering logic per platform: -- a_class=subscription: show nodes whose subscriptionName is in platform.subscriptionNames; empty subscriptions = show all (new platform default) -- a_class=region: show nodes whose region is in platform.region_filters -- a_class=quality: show all healthy nodes (Top-N ranking is card-internal) -- a_class=manual: show nodes whose node_hash is in platform.manualNodes - -Both subscription and region viewMode share the same per-node filter helper `isNodeSelectedByAnyPlatform(node, platforms, groupKey)`. - -### S2 — New-platform default strategyConfig entry (Q2) - -In sync() merge block (L774-792), if Resin returns a platform not in strategyConfig, auto-create default entry: `{ platform_name, a_class: "subscription", b_class: "random", subscriptions: [], regions: [] }`. This auto-entry is ephemeral (written to in-memory merge only, not persisted to strategyConfig JSON until user explicitly configures). - -### S3 — buildEdges B->C edge by a_class semantics (Q5) - -buildEdges B->C edge logic per platform: -- a_class=subscription + subscriptions=[] -> edge to ALL subscription/region groups (unconfigured = unrestricted) -- a_class=subscription + subscriptions=["subA"] -> edge to subA groups only -- a_class=region + regions=["fi"] -> edge to fi region groups only -- a_class=quality -> edge to ALL groups (quality considers all healthy nodes) -- a_class=manual -> NO group-level edge (manual_nodes is node-level, not group-level) - -### S4 — getSelectedRegions removal (Q7) - -Delete getSelectedRegions (L104-110). Remove selectedRegions useMemo (L859). buildCColumnGroups receives platforms directly. No dead code residue. - -### S5 — Test closed-loop (Q6) - -12 new vitest assertions: -- isNodeSelectedByAnyPlatform: 4 a_class branches + empty subscriptions = all + missing aClass default -- buildEdges a_class: subscription+empty = all edges, subscription+specific = selective, region = selective -- sync merge: Resin platform not in strategyConfig -> default entry auto-created - -## Rejected alternatives - -- B) C-column shows all nodes, region filtering only on edge connection (rejected: visual clutter, user loses at-a-glance "what does this platform see" information) -- C) region_filters authoritative source stays Resin backend, not strategyConfig overlay (rejected: breaks ADR-0036 single-source-of-truth for strategy) - -## Consequences - -- Canvas C-column reflects per-platform a_class strategy semantics, not global region union -- New platforms appear in B column with edges to all subscription groups (unconfigured = unrestricted) -- strategyConfig JSON remains the whitebox source of truth; sync merge auto-fills missing entries in-memory -- Manual mode platforms have no B->C group edges (manual selection is node-level via card checkbox, ADR-0042 S4) diff --git a/docs/adr/0053-tauri-specta-adoption-pilot.md b/docs/adr/0053-tauri-specta-adoption-pilot.md index bfef8d50..451d0c29 100644 --- a/docs/adr/0053-tauri-specta-adoption-pilot.md +++ b/docs/adr/0053-tauri-specta-adoption-pilot.md @@ -1,6 +1,6 @@ # ADR-0053: tauri-specta 采纳试点 — 仅类型层采纳,分期迁移 -Status: PROPOSED (检查点 B 待用户拍板 — 三选一结论在 §Decision,推荐方向已按 handoff 默认先行) +Status: Accepted (type-layer pilot only; runtime generation rejected — adjudicated r12-wave-j D-006; status field corrected 2026-10-15) > Date: 2026-08-30 > Ticket: architecture-recovery 09 (tauri-specta-pilot) > Related: ADR-0045 (IPC 错误契约 — IpcError 判别联合由此进入 bindings.ts)、 diff --git a/docs/adr/0075-canvas-write-read-model-fault.md b/docs/adr/0075-canvas-write-read-model-fault.md deleted file mode 100644 index accdf1ec..00000000 --- a/docs/adr/0075-canvas-write-read-model-fault.md +++ /dev/null @@ -1,107 +0,0 @@ -# ADR-0075: Canvas write/read model fault — subscription intent vs region bundle - -Date: 2026-09-22 -Status: Accepted -Supersedes: partial write-path of ADR-0048 (read side only) -Provenance: renumbered from branch `fix/zcode-GUI-Topology` ADR-0052 -(recovered from commit `4d9fa39e`, reimplemented for the CanvasV4 / -deep-IPC seam on main; the branch version wrote `strategyConfig` via -`strategy_config_put`, main's sanctioned path is the -`strategy_platform_subscriptions_set` deep edit — semantics identical, -write seam different). Re-validated against main 2026-09-22: every defect -below was live on main. - -## Context - -The canvas had **one generation writing** and **another generation -reading** strategy data: - -| Layer | Generation | Behavior | -|---|---|---| -| **Write path** (`onConnect`, `onEdgesDelete`) | Gen-1 | Treated a dragged subscription group as a **region bundle**: expanded `sg.regions` into `region_filters` via `strategy_platform_regions_set`. Never wrote the `subscriptions` field. | -| **Read path** (`buildEdges`, `buildRegionViewEdges`) | Gen-2 (ADR-0048) | Judged edges by `subscriptions`: empty -> unconditional `"subscription:all"` label. Since the write path never filled it, every bound platform displayed `subscription:all`. | -| **Backend** (`strategy_engine.rs`) | Gen-2 (correct) | `a_class_regions` Subscription branch derives regions FROM `subscriptions`. Empty = all. **No engine change needed.** | - -Second symptom — **edge scramble**: edge ids were identical across view -modes, so ReactFlow `EdgeWrapper` reused stale geometry for 1-2 frames on -view switch (xyflow #2973 measurement timing; #693: unique ids are the -only working fix). - -## Decision - -### 1. Write subscription INTENT, not region projection - -`onConnect` subscription branch writes the subscription list and stamps -`a_class=subscription` — via the new sanctioned deep write -`strategy_platform_subscriptions_set` -> `StrategyService:: -set_platform_subscriptions` (mirrors `set_platform_regions`). It no -longer expands `sg.regions` into `region_filters`; region derivation -belongs to `compute_plan` at apply time. - -`onEdgesDelete` subscription branch mirrors this: removes the -subscription from `subscriptions` (not `sg.regions` from -`region_filters`). - -New seam added this ticket: service fn + `#[tauri::command]` + -headless `PATCH /api/v1/shell/strategy/subscriptions` + capability + -ipc.ts wrapper `ipcStrategyPlatformSubscriptionsSet`. - -### 2. Unconditional edges carry NO label - -Per the Kiali/Grafana/LangGraph convention (default/unrestricted flows -carry no label; only conditional/exception edges are labeled): - -- Empty `subscriptions` -> edge exists with **no label** (was - `"subscription:all"`) -- Specific `subscriptions` -> label `"subscription:<name>"` -- `quality` carries no user-specified condition in `PlatformStrategy` -> - unconditional -> **no label** (was `"quality:all"`; adjudicated in the - r12-wave-a audit rework: the invariant covers every unconditional edge, - not just subscription ones) - -All `*:all` unconditional literals removed from `buildEdges` and -`buildRegionViewEdges`. - -### 3. Edge id viewMode prefix - -- `buildEdges` (subscription viewMode): `e-sub-{plat}-{sub}` -- `buildRegionViewEdges` (region viewMode): `e-reg-{plat}-r-{region}` - -This forces `EdgeWrapper` remount on viewMode switch, eliminating stale -geometry. - -## Implementation notes - -- Subscription/region/quality edges keep `deletable: false` (CONTEXT.md: - only manual edges can be dragged/deleted). React Flow v12 filters - `deletable:false` upstream, so the `isSub` arm inside `onEdgesDelete` - is latent code — kept as documentation of intent-removal semantics if - the domain rule ever changes. -- The three patch helpers intentionally stay non-abstracted: each writes - a different field with its own default `a_class`; a generic helper - would add parameters without behavioral payoff (Fowler "Duplicated - Code" judgment call). - -## Known leftover (decided: no auto-migration) - -Platforms dragged with the old Gen-1 write path have `regions` populated -but `subscriptions` empty -> they show unconditional (unlabeled) edges -until the user re-drags the subscription group once. Auto-migration was -rejected: the recorded `regions` remain a valid region strategy; -converting would be lossy. - -## Test coverage - -Vitest `TopologyView.test.tsx` ADR-0075 describe block: specific subs -> -`subscription:<name>` label; empty subs -> unlabeled edges; `e-sub-` / -`e-reg-` id prefixes; cross-viewMode B->C ids never collide; no -`subscription:all` literal anywhere. - -## References - -- xyflow issue #2973 (ResizeObserver measurement timing), #693 (edge id - uniqueness fix) -- Kiali/Grafana/LangGraph default-flow-unlabeled convention -- CONTEXT.md glossary: "Subscription Intent" + "Strategy-Labeled Edge" -- ADR-0048 (Gen-2 read path), ADR-0036 (strategyConfig single write - authority), ADR-0076/0077 (companion recovered fixes) diff --git a/docs/adr/0077-canvas-node-below-platform-three-defects.md b/docs/adr/0077-canvas-node-below-platform-three-defects.md deleted file mode 100644 index cf3c64b4..00000000 --- a/docs/adr/0077-canvas-node-below-platform-three-defects.md +++ /dev/null @@ -1,87 +0,0 @@ -# ADR-0077: Canvas node-below-platform bug — three real-runtime defects - -Date: 2026-09-22 -Status: Accepted -Supersedes: none (extends ADR-0048 region-viewMode edge logic; the -ADR-0076 manual-edge fix is a separate doc/code drift) -Builds on: ADR-0002 (three-column canvas), ADR-0048 (a_class-semantic -filtering) -Provenance: renumbered from branch `fix/zcode-GUI-Topology` ADR-0051 -(recovered from commit `255eb7b8`; the original diagnosis was made via a -live sidecar on real state/cache DBs). Re-validated against main -2026-09-22: all three defects were live on main (camelCase-only tag -lookup, no column anchors, missing subscription+specific-subs region -branch). - -## Context - -Symptom: selecting a subscription or region renders the C-column group -nodes BELOW the platform card instead of to its right — in both -subscription and region viewMode. - -## Root cause (three defects, all required to reproduce both viewModes) - -### Defect A — subscription name parsing mismatched the Resin API - -Resin `/api/v1/nodes` returns node tags as snake_case: -`{ subscription_id, subscription_name, tag }`. -`parseSubscriptionGroups` looked up ONLY `t.subscriptionName` -(camelCase), which never matched, so groups fell back to `tags[0].tag` -(the per-node tag string, e.g. "1/node-01"). strategyConfig -`subscriptions: ["1"]` then never matched any group -> zero B->C edges --> the C group disconnected -> dagre stacked it at rank 0 (below/left). - -### Defect B — port-less platform amplified the stack (layout guard missing) - -A platform with no entry ports bound (auto-default `subscription` + -empty subscriptions) has no A->B edge -> sits at rank 0, and its -unconditional edges drag every C group into rank 1 — the same column as -the WITH-ports platform — so C groups appear directly under the Default -card in both viewModes. - -### Defect C — region viewMode skipped subscription-with-specific-subs - -The region-viewMode edge loop only handled `subscription` aClass when -`subscriptionNames` was empty; a platform with `subscriptions: ["1"]` -drew NO region edges -> regionGroup disconnected -> same rank-0 stack. - -## Decision - -1. **Defect A**: `NodeItem.tags` accepts `subscription_name` (snake) - alongside `subscriptionName`; `parseSubscriptionGroups` reads - `t.subscriptionName ?? t.subscription_name`. -2. **Defect B**: `layoutNodesViaDagre` adds three invisible dagre-only - column anchors (`__col0 -> __col1 -> __col2`, `minlen:0` same-rank - binding to entryPort/platform/C-group). Anchors are never returned as - ReactFlow nodes. Even with no real edges (port-less platform, - unmatched subscription, snapshot load failure) dagre now ranks C at - col2. Pattern: idootop/reactflow-auto-layout `#root` anchor + Kiali - BoxLayout synthetic edges. -3. **Defect C**: the region-viewMode edge loop is extracted to the - exported pure helper `buildRegionViewEdges`; its `subscription` - branch also handles non-empty subscriptions (edges to the regions of - the named subscriptions), mirroring `buildEdges` semantics. - -## Rejected alternatives - -- elkjs layerConstraint / Grafana grid — heavier, new dependency. -- Hide port-less platforms — loses real topology info; the anchor is the - structural fix and also covers load-failure/unmatched cases. - -## Consequences - -- The three-column contract (ADR-0002) now holds structurally for ALL - aClass/viewMode/edge combinations, including degenerate cases. -- `buildRegionViewEdges` is exported and unit-tested (was inline). - -## Upgrade path (carried from branch, atomcode re-verification) - -`@dagrejs/dagre >=1.1.5` ships official manual ranking (PR #271), but -the merged feature is thin (+7/-1 lines, zero tests/docs) with known -crash modes (same-rank real edges, missing `rank` attrs). Stay on -`dagre@0.8.5` + the anchor pattern; if rank pinning is ever needed, -spike on a separate branch — do not hand-patch 0.8.5. For cascading -delete there is an official path: `useReactFlow().deleteElements` -routes through `getElementsToRemove` (respects `deletable:false`) + -`onBeforeDelete` veto — prefer that over growing custom helpers if edge -deletion semantics ever expand. diff --git a/docs/adr/0078-acceptance-line-dual-deployment-profiles.md b/docs/adr/0078-acceptance-line-dual-deployment-profiles.md index 8f6f1e35..62f9aba4 100644 --- a/docs/adr/0078-acceptance-line-dual-deployment-profiles.md +++ b/docs/adr/0078-acceptance-line-dual-deployment-profiles.md @@ -1,6 +1,6 @@ # ADR-0078: Acceptance line - dual deployment profiles, dual thresholds, measured-vs-smoke semantics -Status: DRAFT (R12-00; awaits representative-hardware numbers before locking `†` absolute pins) +Status: Accepted (R12-00 acceptance contract live in bench.yml; `†` absolute pins remain provisional pending representative-hardware numbers) - **Date**: 2026-09-22 (r12-wave-a grill decision-ledger D-001/D-002; execution ticket R12-00) - **Supersedes**: the round-8 D-004 single-threshold perf budget table (its locks remain recorded as `locked` columns in `scripts/bench/acceptance.json`) - **Research basis**: cloudflare-streaming-resources-1, tail-flicker-flicker-1 indexed research sessions (TTFT / event-gap / backpressure calibers) diff --git a/docs/adr/0079-resin-positioning-value-boundary.md b/docs/adr/0079-resin-positioning-value-boundary.md index 37166a3a..24fe6fd4 100644 --- a/docs/adr/0079-resin-positioning-value-boundary.md +++ b/docs/adr/0079-resin-positioning-value-boundary.md @@ -1,7 +1,7 @@ # ADR-0079: Resin positioning — shell-is-the-product, engine-is-a-seam (evaluation record) Date: 2026-09-22 -Status: Draft (evaluation output, NOT a refactor authorization — R12-06) +Status: Accepted (positioning boundary; evaluation-document origin — still NOT a refactor authorization, R12-06) Provenance: R12-06 ticket (r12-wave-a D-003④). Resolves the standing question "is the real product shape just Resin itself, and where does the shell's value boundary sit?" diff --git a/docs/agents/README.md b/docs/agents/README.md index b3578a04..b3129532 100644 --- a/docs/agents/README.md +++ b/docs/agents/README.md @@ -3,13 +3,53 @@ Files here are **agent instructions** (read by AI agents when executing code tasks). They are NOT user documentation. For user docs, see `docs/` parent. +## Agent operating rules (mandatory) + +### context-mode routing + +- File edits (including patches) go through `ctx_batch_execute` / `ctx_execute_file`, not apply_patch. +- `ctx_*` first; fall back to builtins only when `ctx_*` can't do the job. Read-to-analyze / search / large grep: `ctx_batch_execute` or `ctx_search` — never Get-Content/Select-String into context. For data analysis use `ctx_execute(code)` and print only the answer. +- Web/HTTP: `ctx_fetch_and_index(url, source)` then `ctx_search(queries)`. curl/wget/inline HTTP are forbidden. +- Shell is fine for bounded execution: git ops, mkdir, rm, mv, cd, ls, builds, tests. +- Windows paths in the ctx sandbox: forward-slash form `D:/Aworker/EgressAPIKEY/...`; the `shell` language routes to `pwsh.exe` (PowerShell 7), NOT bash; inline `$` is stripped by the host transport — put PowerShell logic in a `.ps1` and run with `-File`. +- After resume: `ctx_search(sort:"timeline")` before asking the user anything. +- Output artifacts as files + path + one-line summary; never inline large content into chat. + +### CodeGraph (first step for code exploration) + +The index lives at `.codegraph/` (gitignored). Before reading files for "how does X work / where is X / what calls Y", query CodeGraph first: + +- MCP: `codegraph_explore` with `projectPath: "D:\Aworker\EgressAPIKEY"` +- CLI: `codegraph explore "<query>"` / `codegraph query "<symbol>"` / `codegraph node <symbol>` / `codegraph files` +- After any code change, run `codegraph sync .` in the same session (recommended, not a gate — CI holds no index) +- Treat codegraph-returned source as already-Read; do not re-open those files in the same session + +### File integrity + +- New files: UTF-8 no BOM, LF endings (CRLF allow set: `.bat`, `.ps1`, `.cmd`). `git diff --check` must be clean before commit. +- Re-read the affected region before destructive edits; after two failed patch attempts, do one whole-file rewrite and verify bytes. +- Never inline `$` PowerShell logic; write a `.ps1` and run with `-File`. +- Session-local dirs (`.scratch/`, `.codex-tmp/`, `.zcode/`, `.omx/`, `context-mode/`) never enter git tracking — each has a literal `.gitignore` line; untrack leftovers with `git rm -r --cached <path>` (files on disk are preserved, never deleted). + +### Working etiquette + +- If uncommitted changes appear that this agent did not make, treat them as user/external — do not revert them. +- Subagents: off by default; use them only when the session's user enables them, otherwise execute single-threaded. +- AtomCode research runs are serialized — one active run at a time. + +## Index + | File | Purpose | |---|---| -| `architecture-state.md` | Runtime wiring, sidecar lifecycle, ResinClient, IPC retarget, CI pipeline (AGENTS.md §11-17) | -| `phase-history.md` | Implementation phase records P9-P26, T7-T22, R1-R2, C1-C2 (AGENTS.md §18-66) | +| `architecture-state.md` | Runtime wiring, sidecar lifecycle, ResinClient, IPC retarget, CI pipeline | | `issue-tracker.md` | Issue tracker declaration: local markdown under `.scratch/`, switch path to GitHub Issues | -| `domain.md` | Domain doc layout (single-context: `CONTEXT.md` + `docs/adr/`) and consumer rules | +| `domain.md` | Domain doc layout (`CONTEXT.md` + `docs/adr/`) and consumer rules | | `triage-labels.md` | Triage label vocabulary: five default state roles | +| `trigger-line-register.md` | Live register of armed/parked trigger lines + write-back history | +| `evidence/` | Frozen evidence bundles cited by register rows | +| `storage-locations.md` | L1/L2/L3 config layers, key inventory, write paths | +| `ipc-manifest.md` | Machine-checked IPC command manifest (regenerated — do not hand-edit) | +| `community-files.md` | CONTRIBUTING / ISSUE_TEMPLATE / PR_TEMPLATE / SECURITY / CODE_OF_CONDUCT inventory | +| `round12-wave-l-next-round.md` | Live committed watch taskbook | -These files were extracted from AGENTS.md to keep the root file under the 32 KiB -Codex `project_doc_max_bytes` limit (progressive disclosure principle). +`phase-history.md` was retired 2026-10-15 (process narrative; phase outcomes live in `CHANGELOG.md` and git history). diff --git a/docs/agents/phase-history.md b/docs/agents/phase-history.md deleted file mode 100644 index 1333507e..00000000 --- a/docs/agents/phase-history.md +++ /dev/null @@ -1,805 +0,0 @@ -# Phase History — Agent Instruction Layer - -> Purpose: historical record of implementation phases (P9-P26, T7-T22, R1-R2, C1-C2). -> These are **process artifacts**, not current truth. For current architecture, see `docs/architecture/`. -> For current decisions, see `docs/adr/`. For what shipped, see `CHANGELOG.md`. -> Extracted from AGENTS.md to keep root file under 32 KiB (progressive disclosure). -> Source: AGENTS.md ### sections 18-66, content unchanged (extraction only). - ---- - -## 18. P9 hardening - process routing, log rotation, test closed-loops, i18n seed sync - -- **Process routing (issue 3 + 10)**: three new `#[tauri::command]` in `src-tauri/src/commands/mod.rs` now own the per-process -> lane routing-rule registry. - - `process_route_add(app, process, target_lane)` - validates name (1..128 chars, no control per AGENTS <language uri>§7.5), lane range `0..MAX_LANES=50`, persistence via `tauri-plugin-store` key `processRoutes`. REJECTS via a pure helper `process_route_conflict_check(existing, new_process, new_lane)` BEFORE recording if the same target lane is already bound to a DIFFERENT process - the conflict surfaces as a typed `Err(String)` to the IPC caller. - - `process_route_remove(app, process)` - deletes by name. - - `process_route_list(app) -> Vec<ProcessRouteRule>` - reads persistent rules; the frontend rehydrates from this on mount. - - The `ProcessRouteRule { process: String, target_lane: usize }` struct is `Serialize + Deserialize` so it round-trips through serde_json::Value (AGENTS §7.6 - server-trusted store, never exposed via a raw-String constructor in the webview). - - Frontend: `src/views/ProcessRouteView.tsx` round-trips through the IPC wrappers `ipcProcessRouteAdd/Remove/List` in `src/lib/ipc.ts` (TS-boundary lane-range + name guards per AGENTS §7.6). On conflict the UI surfaces `t("processRoute.conflict")` as a red toast; on success `t("processRoute.backendSaved")`. -- **Log hardening (issue 11)**: `src-tauri/src/main.rs` configures tauri-plugin-tracing with `with_rotation(Rotation::Daily)` + `with_max_file_size(MaxFileSize::mb(10))` + `with_rotation_strategy(RotationStrategy::KeepSome(7))` so a runaway log stream cannot overflow the user's disk past 7 daily files or 10 MB each. A `std::panic::set_hook` installed at the top of `fn main()` captures panics into the same `tracing::error!` pipeline with a backtrace so a crashed sidecar thread surfaces a log line instead of silently unwinding; the original hook is chained. -- **Settings unified save (issue 6)**: `src/views/SettingsView.tsx` now exposes a single sticky bottom Save bar covering lanes/gatewayBind/mihomoApi. The previous per-card Save buttons are removed (comments keep their place). The unified Save sets a `busy` state and disables the button until the round-trip completes; it uses the existing `saveAll()` body unchanged. -- **Test closed-loops (issue 1)**: - - `src/lib/subDrag.test.ts` - closed-loop coverage of the drag-reorder helper (begin / enter / finish / cancel + right-button ignore). - - `src/lib/ipc.test.ts` - mocks `@tauri-apps/api/core::invoke` and asserts: (1) platform_add/remove/list forward the right command name with the right args; (2) account_add LANE_OUT_OF_RANGE guard fires BEFORE invoke; (3) subscription_add URL-shape guard fires BEFORE invoke; (4) process_route_add/Remove/List forward + the lane-range guard; (5) empty-name / control-char rejection at the TS boundary. - - `src-tauri/src/commands/mod.rs` `#[cfg(test)]` module: `process_route_conflict_rejects_same_lane_different_process` tests the pure conflict helper on three permutations (same-name same-lane ok / different-name same-lane rejected / different-name different-lane ok). - - `pnpm test` = 24 tests pass across 4 files (was 6 across 1). -- **i18n seed fix (issue 5)**: 52 stale seed values previously untranslated (the generator had dumped the en value verbatim). `subscription.name` + `subscription.description` + `topology.sidecarUnhealthy` are now native across all 18 locales; `pnpm i18n:check` = 83 keys all match (was 75 before the 8 key adds for the conflict/backend-saved/entry/keyHash/sseLocked/unifiedHelp/importSuccess/dragHint categories). -- **Host-triple layout note**: the host triple depends on installed toolchain (check with `rustc -vV`; currently `x86_64-pc-windows-msvc`, was previously `x86_64-pc-windows-gnu`), so cargo emits the release binary to `target/<host-triple>/release/EgressAPIKEY.exe` (NOT necessarily `target/release/EgressAPIKEY.exe`). The `tauri build` bundler looks at `target/release`, which is why the installer-bundle step fails on this host ("can't open main binary"). The portable path in `scripts/build-all.sh` is the canonical smoke target (uses `find` to locate the binary in either layout); the installer step is now best-effort (won't fail the script if the bundler cannot find the flattened path). - -### 19. P10 audit fixes - topological data, subscription retry, settings consolidation - -- **Issue 6 (settings duplicate Save)**: the per-card Save button inside the Settings Network section was removed; only the unified sticky bottom bar calls `saveAll()` so a user commits network + lanes + network-specific changes to `tauri-plugin-store` in one action. Verified `SettingsView.tsx` no longer has the inline button - just the bottom bar. -- **Issue 8 (subscription import shows 0 nodes)**: `SubscriptionsView.tsx` now has `refreshWithRetry()` which polls the live `/api/v1/subscriptions` list 3 times at 1s intervals so the displayed `node_count` reflects Resin's async subscription fetch (the sidecar fetches remote subscriptions in the background; 500ms was too short for the user's test URL -> "Imported 0 nodes"). On persistent 0 the final list snapshot is set so the user always sees the latest backend state. Resin's design: `update_interval: 5m`, forced full refresh on startup ("Subscription scheduler started; forced full refresh running in background" log line). Polled not 5m because the first poller run takes seconds, not minutes. -- **Issue 4+7 (topology canvas shows empty lane boxes)**: `TopologyView.tsx` now builds Entry boxes with a per-platform active-lease count (`topology.leasesActive` i18n key, added to all 18 locales). Lane boxes already render `busy/free/exitIp/keyHash/sseLocked`; the live-server lease count comes from the Resin `/metrics/realtime/leases` endpoint (verified: `{"items":[{"active_leases":0,"ts":"...","platform_id":""}], "platform_id":"","step_seconds":5}`). Resin's public API does NOT expose per-key/per-lane exit IPs through the leases endpoint today - it exposes per-platform aggregate `active_leases`. The desktop shell faithfully mirrors what Resin returns; per-lane/<key-hash mapping stays internal to the Go sidecar by design (DESIGN.md token->lane hashing runs in-process). The user-visible improvement: entries show "入口: OpenAI / 活跃租约: 3" instead of an empty label. -- **Live Resin API verification (transient test only, not committed)**: spawned `resin-x86_64-pc-windows-gnu.exe` locally, ran `POST /api/v1/platforms {"name":"OpenAI-Test"}` -> 201 with new UUID; `POST /api/v1/subscriptions` with the user's URL -> 201; both stored in sidecar SQLite state.db. node-pool stayed 0 because the remote subscription fetch is async and needs seconds-to-minutes for the first full refresh. This confirms the IPC layer is functionally correct - the user-visible "0 nodes" / "platform does nothing" behavior was the async fetch latency, not a broken wire. -- **i18n**: 84 keys across 18 locales (was 83; +1 `topology.leasesActive`). `pnpm i18n:check` green. -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 10.9MB release exe staged at `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar (required for tauri-plugin-shell sidecar path resolution at runtime). Smoke: pid alive, resin.exe child alive; MainWindowTitle=EgressAPIKEY. -- **Tests**: 24 vitest, 54 cargo, all green. No new tests yet; the next phase needs component closed-loop tests plus an integration e2e that asserts the add-platform -> list-platform round trip against the live sidecar. - -### 20. P11 hardening - per-platform lease resolution, test framework, process-route limitation note - -- **Per-platform active leases (T2)**: `LaneSnapshot` now carries `pub per_platform_active: Vec<(String, usize)>` populated by `gateway_snapshot` from Resin `/api/v1/metrics/realtime/leases` + `/api/v1/platforms`. New pure helper `per_platform_active_from_leases()` maps the lease `platform_id` (UUID) to the platform `name`; empty `platform_id` resolves to the Default platform; an unknown UUID surfaces the raw id so the canvas still renders. Two new unit tests in `src-tauri/src/commands/mod.rs`: `per_platform_active_resolves_uuid_to_name`, `per_platform_active_falls_back_to_raw_id_when_unknown`. `TopologyView.tsx` renders the per-platform lease count into the Entry box. -- **Test framework upgrade (T1)**: `vitest.config.ts` switched to jsdom + shared setup `src/test/setup.ts` (global Tauri IPC `invoke` mock + i18n init). Three new per-view closed-loop component tests against the IPC mock: `PlatformsView.test.tsx`, `SubscriptionsView.test.tsx`, `ProcessRouteView.test.tsx`. `pnpm test` = 32 tests across 6 files (was 24 across 4). `pnpm exec tsc --noEmit` green. -- **Process-route limitation note (T3)**: Resin Go sidecar exposes NO public per-process routing API (live `/api/v1/routes` returns 404; perplexity research concurred). The desktop shell persists rules to `tauri-plugin-store` and the Rust side rejects lane conflicts BEFORE recording (`process_route_conflict_check`), but per-process enforcement only happens when client traffic flows through the Resin proxy (sidecar owns per-request auth). `ProcessRouteView.tsx` now surfaces a callout with the new `processRoute.proxyNote` i18n key (added across all 18 locales; `pnpm i18n:check` = 85 keys match). Documented here so a future agent does not claim the shell owns routing enforcement. -- **Host test caveat (unchanged)**: `cargo test -p egressapikey-app --features custom-protocol --lib` still exits `STATUS_ENTRYPOINT_NOTFOUND (0xc0000139)` on THIS dev host (Tauri native-plugin DLL linker issue, environmental NOT code). `cargo build -p egressapikey-app --features custom-protocol` is green; `cargo test -p resin-core --lib` = 54 passed (the app-crate `per_platform_active_*` tests are logic-verified by build + the shared pure helper shape, and `ResinClient` mockito tests cover the offline round trip). The release-exe smoke harness remains the live-side check. - -### 21. P12 documentation drift fix - dead paths + stale dependency - -- **Audited via codegraph** (no manual grep+Read loop): verified `boot_resin`, `spawn_health_poll`, `SidecarHandle`, `gen_token`, `per_platform_active_from_leases`, `process_route_conflict_check`, `ResinClient`, `MihomoController`, `sanitize_lanes`, `MAX_LANES`, `LaneSnapshot`, `GatewayState` all exist on disk at the line numbers AGENTS cites; the only stale doc claims were the ones fixed here. -- **README.md L27**: `docs/MEMORY.md` referenced a file that never existed in git history (`git log -- docs/MEMORY.md` empty). The repo's compressed-research-memory document is `docs/architecture/MEMORY_REUSE_DECISION.md`. Fixed the anchor + both READMEs. -- **README_CN.md L58**: same dead `docs/MEMORY.md` anchor, fixed to `docs/architecture/MEMORY_REUSE_DECISION.md`. -- **Stale `petgraph` in tech-stack lists**: `git grep -l petgraph` returns only `AGENTS.md`, `README.md`, `README_CN.md` (three doc rows) — NO Cargo.toml and NO source reference. Removed `petgraph` from: AGENTS.md L44 (Backend core), README.md L38 (Backend row), README_CN.md L36 + L51 (two backend rows). `rusqlite`, `tokio`, `axum`, `reqwest` all re-verified present in `crates/resin-core/Cargo.toml`. -- **Preserved-as-history (NOT changed, deliberate)**: AGENTS §19 "24 vitest" / "i18n 84 keys" and §11 "SubscriptionsView.tsx and ProcessRouteView.tsx are still pure local Zustand (no invoke())" are superseded by §16 / §18 / §20 in the same file. Left intact because they are per-phase status snapshots; a top-down reader hits §20 (P11) for the current truth. Editing historical status rows would erase the audit trail that AGENTS §6 / §10 protect. If a future reader needs a single current-state row, that is the job of §20 (or a new section), not a rewrite of §19. -- **Verified post-edit**: LF clean (CRLF 0 in README/README_CN/AGENTS), `pnpm exec tsc --noEmit` green, `pnpm test` = 32 tests / 6 files, `pnpm i18n:check` = 85 keys / 18 locales, `cargo test -p resin-core --lib` = 54 passed, `cargo build -p egressapikey-app --features custom-protocol` green. No code changed, so no release rebuild needed for this doc-only commit. - -### 22. P13 - subscription/platform real backend loop + log observability - -- **B6 + B4 root cause (items-wrapper bug)**: Resin wraps every list endpoint as `{"items":[...], "total", "limit", "offset"}`, NOT a bare array. The old `platform_names` / `platform_id_for_name` / `subscription_snapshot` / `subscription_id_for_name` helpers in `src-tauri/src/commands/mod.rs` used `v.as_array()` which is always `None` for the items shape - so `platform_list` returned `[]` (B6 list never populated) and `subscription_list` returned `[]` (B4 node count always 0). Fix: new shared helper `fn items_arr<'a>(v: &'a serde_json::Value) -> &'a [serde_json::Value]` accepts BOTH the items-wrapper and a bare-array back-compat path; all four helpers now call it. `per_platform_active_from_leases` was also rewritten to use `items_arr(platforms)` (the old `platforms.as_array()` had the same bug). 7 new unit tests pin both shapes. -- **B4 real backend loop (clash UA + flow->block convert + local POST)**: `crates/resin-core/src/resin_client.rs` ships two new free functions: - - `pub async fn fetch_clash_subscription(url) -> Result<String>` builds a separate reqwest::Client (15s timeout, 5 redirects) and rotates 4 clash-family UAs. Many subscription providers (the user test host included) return 403 to a default UA but 200 + full Clash YAML to a clash client UA. Resin's own remote-fetch uses a default UA and was getting 403 - so its `node_count` stayed 0 forever. We bypass it: the shell fetches the YAML itself. - - `pub fn clash_yaml_to_proxies_block(input) -> Result<String>` is a hand-written state machine (~140 lines, NO serde_yaml dep). It extracts the top-level `proxies:` segment, rewrites every flow-style `- { k: v, k: v }` list item into block-style, drops `proxy-groups`/`rules`/`dns`. Resin's Go YAML parser chokes on flow-style inline mappings and on unicode group names; this is the narrowest transform that fixes the user's 53-proxy test subscription. `fn split_flow_fields(s)` handles single-quote-escaped commas inside values. - - `subscription_add` command rewritten: fetch YAML -> convert -> POST `{name, source_type:"local", content:<block>, url, update_interval:"30s"}` to Resin. 30s interval so Resin's scheduler parses the local content on the first tick (seconds, not the default 5m); Resin has no public force-refresh endpoint. Every step emits `tracing::info!`/`tracing::warn!` into the same log file (B5). -- **B5 log observability**: the existing `tauri-plugin-tracing` rotation (daily, 10MB, keep 7) already covers the file appender; the new `subscription_add` path adds `tracing::info!(subscription, url, "fetching clash yaml")`, `tracing::info!(bytes, "fetched yaml, converting")`, `tracing::info!(block_bytes, "posting local subscription")`, plus `tracing::warn!` on fetch/convert/POST failure. Logs land at `C:\\Users\\Administrator\\AppData\\Local\\com.egressapikey.desktop\\logs`. No new crate. -- **B1 toast i18n (interpolated, not hardcoded)**: `SubscriptionsView.tsx` no longer concatenates English; `subscription.importSuccess` across all 18 locales now carries `{{total}}` (en: "Subscription imported - {{total}} nodes", zh: "订阅导入成功 - 共 {{total}} 个节点", + 16 others). Frontend calls `t("subscription.importSuccess", { total })`. Closed-loop test: "B1: shows interpolated importSuccess toast with total nodes after import". -- **B2 form layout + draft persistence**: `SubscriptionsView.tsx` layout switched from a flex row (which collapsed: the description span moved under the name input and the URL box gap appeared empty after remount) to two stacked `grid grid-cols-1 gap-1.5` rows: name + description above, URL + import button below. Form draft (name + url) now persists to a new `appStore.subFormDraft: {name, url}` + `setSubFormDraft` setter via a `useEffect`, so navigating away+back keeps the input contents. Closed-loop test: "B2: form draft persists across remount". -- **B3 drag-reorder wired for real**: rows had `onMouseDown`/`onMouseEnter` calling `beginDrag`/`enterTarget`, but releasing outside the row never fired `finishDrag` (onMouseUp was on the `<li>`). Added a window-level `mouseup` listener (registered only while `drag.isDragging`) that calls `finishDrag` on `setLive`. Dragging a row now grays it out (`opacity-50` while `dragIndex === i`) and commits the reorder on release anywhere in the window. -- **B6 platform list refresh + toast i18n**: after the items-wrapper fix, `ipcPlatformList` returns real names, so the existing `PlatformsView.refresh()` in `handleAddPlatform` actually populates the list. The toast strings move to i18n: `t("platform.add") + " OK"` -> `t("platform.addOk")` (new key across 18 locales: en "Platform added", zh "平台添加成功", + 16 others). -- **Closed-loop test gate B4 (key->lane->distinct exit-IP contract)**: `crates/resin-core/src/lane.rs` new test `distinct_keys_use_distinct_lanes_contract`: samples 20 distinct keys, asserts every lane index < 50, the 20 keys use >= 10 distinct lanes (low collision), and the distribution is non-degenerate. This is the shell-side statement of the per-key hash->lane->fresh-TCP->distinct-exit-IP contract; the live Resin sidecar enforces it natively via token->account binding. -- **Tests**: `cargo test -p resin-core --lib` = 59 passed (was 54; +3 `clash_yaml_to_proxies_block_*`, +1 `split_flow_fields_handles_quoted_comma_inside_value`, +1 `distinct_keys_use_distinct_lanes_contract`). `pnpm test` = 34 across 6 files (was 32; +2 B1/B2 closed-loop component tests). `pnpm i18n:check` = 86 keys / 18 locales (was 85; +1 `platform.addOk`). `pnpm exec tsc --noEmit` + `pnpm build` green. `cargo build --release -p egressapikey-app --features custom-protocol` -> 10.9MB release exe staged at `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar (37.8MB). Smoke: MainWindowTitle="EgressAPIKEY", `resin.exe` child alive, sidecar control plane up in 0ms, no stderr/panic. -- **Host reproducibility note (B4 live test)**: the user's URL `https://link123.52pokemon66.cc/api/v1/client/subscribe?token=...` returns 403 to a default UA and 200 + 53 Clash proxies to `clash-verge/v2.0.0`. Resin's own remote-fetch uses a default UA and was getting 403 - so even though our old `source_type:"remote"` POST returned 201, the sidecar's background fetch always failed and `node_count` stayed 0. The new local-fetch + flow->block convert + 30s tick path is the root-cause fix. - -### 23. P14/P15 - security audit + topology bug fixes + Phase R1 backend IPC - -- **P14 security audit** (Semgrep + Code-VulnScan + manual taint, report deleted after fix): HIGH path-traversal in `backup_upload` (webview-supplied `zip_path` read arbitrary file + upload to attacker WebDAV) - fixed by canonicalize+starts_with confinement to `app_data/backups`. MED predictable temp-dir path - fixed by writing to `app_data/backups` with crypto-random suffix. LOW IPC `.unwrap()` panic risk in `process_route_add/remove` - replaced with `map_err`. Release exe rebuilt + smoke green. -- **P15 topology bugs**: Bug #1 (topology flash on boot) - App defaulted `view="topology"` so first paint showed TopologyView before `loadView()` resolved the persisted view; fixed with a `bootstrapped` gate that holds a minimal loader until `loadView()` completes. Bug #2 (blank canvas on refocus) - the 5s poll could race/be throttled while window hidden; fixed with a `visibilitychange` listener that re-syncs immediately on refocus. Release exe rebuilt + smoke green. -- **Phase R1 backend IPC** (topology-driven key-to-egress routing, see `docs/architecture/REFACTOR_PLAN.md`): two new `#[tauri::command]` in `src-tauri/src/commands/mod.rs`: `platform_update(name, allocation_policy?, regex_filters?, sticky_ttl?)` -> PATCH `/api/v1/platforms/{id}` (resolves name->id, validates `allocation_policy` against the live-probed enum `BALANCED|PREFER_LOW_LATENCY|PREFER_IDLE_IP`, caps regex_filters at 64 entries/253 chars, caps sticky_ttl at 32 chars); `node_list()` -> GET `/api/v1/nodes` (the "C category" ip channels). Two new `ResinClient` methods: `update_platform(id, body)` + `list_nodes()`. TS wrappers `ipcPlatformUpdate`/`ipcNodeList` in `src/lib/ipc.ts` mirror the Rust validation. 4 new vitest assertions (invalid policy, too many filters, valid forward, node_list forward) + 2 new mockito tests (update_platform PATCH, list_nodes items-wrapper). Tests: cargo 61 passed, vitest 38 passed, i18n 86 keys. Release exe rebuilt + staged + smoke. -- **Live-probed Resin v1.2.0 API surface** (authoritative, not assumed): platforms GET/POST/PATCH/DELETE, subscriptions GET/POST/DELETE, nodes GET (items-wrapper with unique_egress_ips), metrics/realtime/leases, metrics/snapshots/node-pool, healthz. The `allocation_policy` enum is exactly `BALANCED|PREFER_LOW_LATENCY|PREFER_IDLE_IP` (probed: invalid value returns 400 "must be BALANCED, PREFER_LOW_LATENCY, or PREFER_IDLE_IP"). No `/lanes`, `/routes`, `/keys`, `/egress`, `/config` endpoints exist. The key+endpoint identification is Resin-native via `reverse_proxy_fixed_account_header: "Authorization"` + `regex_filters`. - -### 23. P17-R2 - Topology canvas three-column key-to-egress redesign - -- **Phase R2 complete**: the TopologyView is no longer a 2-column entry->lane mirror. It is now a three-column A/B/C key-to-egress routing canvas backed by live Resin platform + node data. -- **A (entry proxy port)**: single blue node on the left, the conceptual Resin forward-proxy entry point. Always-connected to every platform via animated edges. -- **B (platforms)**: one node per platform from `ipcPlatformListFull()` (GET /api/v1/platforms full objects). Each platform node shows name + a sub-line with regex_filters (upstream hosts), allocation_policy, routable_node_count. Custom `PlatformNode` component with left+right ReactFlow Handles. -- **C (IP channels / nodes)**: one node per NODE-REGION-GROUP from `ipcNodeList()` (GET /api/v1/nodes), grouped by region (lowercased from `region` field or 1-3 char tags). Each group node shows region + healthy/total count. Custom `NodeGroupNode` component with left Handle. -- **B->C edges = region_filters binding**: an edge is drawn from a platform to a node-group when the platform's `region_filters` array includes the group's region. This is the faithful mapping of the user's "drag line from platform to IP channel" intent to Resin's actual API (region-based node selection, NOT per-node). -- **Drag-to-connect = live hot PATCH**: `onConnect` detects a connection from `platform-<name>` to `nodegroup-<region>` and calls `ipcPlatformUpdate(name, undefined, undefined, [...current, region])` which PATCHes the platform's region_filters on the live Resin sidecar. The edge appears instantly (optimistic local state update) and Resin picks up the new binding for subsequent requests. Deleting an edge = PATCH region_filters to remove that region. -- **IPC gap fixed (R1 followup)**: `platform_update` Rust command + `ipcPlatformUpdate` TS wrapper now accept a `region_filters` parameter (was missing in R1). Region codes are lowercase ISO 3166-1 alpha-2 or !negation, max 16 chars, max 64 entries, no control/space chars. Validated at both TS and Rust boundaries (AGENTS §7.5). -- **Node schema reality**: Resin v1.2.0 GET /api/v1/nodes returns node_hash, display_tag, has_outbound, failure_count, region, tags. There is NO per-node egress_ip field. The canvas shows display_tag + health; the aggregate egress_ip_count lives only in the node-pool snapshot. This is documented in docs/architecture/REFACTOR_PLAN.md. -- **Bug #1 + #2 (carried from P15)**: topology flash on boot was fixed by the App.tsx bootstrap gate (holds a loader until persisted view is read, so the default "topology" view never flashes before the saved view swaps in). Blank canvas on refocus was fixed by the `visibilitychange` listener that re-syncs on refocus (the 5s poll can be throttled when the window is hidden). Both fixes are preserved in the R2 rewrite. -- **i18n**: 12 new topology keys added across all 18 locales (entryPort, platformsCol, nodesCol, dragHint, region, healthy, unhealthy, noNodes, noPlatforms, policy, routable, filters). `pnpm i18n:check` = 98 keys / 18 locales. -- **Tests**: `TopologyView.test.tsx` (2 new closed-loop tests: renders entry+platforms+node-groups from mocked Resin data; shows empty hints). ResizeObserver polyfill added to `src/test/setup.ts` for jsdom. `ipc.test.ts` +3 tests (regionFilters forwarding, region_filters count guard, updated null-fields expectation). `pnpm test` = 42 pass / 7 files. `cargo test -p resin-core --lib` = 61 pass. -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 10.9MB exe staged at `release/windows-gui/EgressAPIKEY.exe` + resin.exe sidecar (37.8MB). Smoke: MainWindowTitle="EgressAPIKEY", WS 34MB, resin.exe child alive at 63MB. -- **Ponytail**: the canvas does NOT invent a per-node binding API Resin doesn't have. region_filters is the real Resin mechanism; the drag-edge UX maps cleanly onto it. The entry port is a conceptual node, not a fabricated port read — the exact listen port is owned by the sidecar and not needed to draw the A column. - -### 24. P19 - topology viewport memory + subscriptions drag/rename + key+endpoint research + drag-auto-backup + protocol weight research - -- **Item 1: Topology viewport memory**: `src/views/TopologyView.tsx` now wraps the canvas in `ReactFlowProvider` and the inner `TopologyCanvas` uses the `useReactFlow()` hook. On mount we call `loadTopologyViewport()` (new helper in `src/lib/settings.ts` reading `settings.json#topologyViewport` via `tauri-plugin-store`) and `reactFlow.setViewport({x,y,zoom})` before the first data sync. The `onMoveEnd` callback persists the resulting viewport back to the store every time the user finishes a pan/zoom. `fitView` is gated behind `!viewportRestored` so the auto-frame only runs on first paint when no viewport was persisted. -- **Item 6 (drag root cause + rename)**: the original `finishDrag` correctly flipped `live[]`, but the 10s auto-poll `refresh()` immediately replaced the user hand-order with the Resin server sort, so the drag appeared broken despite the lib working. **Fix**: new `localOrder: string[]` state in `SubscriptionsView` plus `loadSubOrder()` / `saveSubOrder()` persistence; `applyOrder(lst, order)` sorts every fresh server batch by the saved local order with new subs appended at the tail. Drag-reorder triggers a window `mouseup` listener that persists the new order. **Rename**: Resin v1.2.0 has no PATCH /api/v1/subscriptions, so `handleRename` deletes the old subscription and recreates a new one with the new name + cached URL (deletes remove a name from the local order, renames replace it in place). A `Reset sort` button next to the drag hint clears the persisted order so the list reverts to the server sort. -- **Item 4 (drag auto-backup)**: every topology drag edit (`onConnect` and `onEdgesDelete`) now calls `backupBeforeEdit()` BEFORE the PATCH. `backupBeforeEdit` is a thin async wrapper around `ipcBackupCreate()` with a swallowed catch: a broken backup should never block the user routing change. This closes the loop topology-drag-change = config write, restart-aware, backup + 防呆 objective for this phase. The user can Settings > Config > Import to roll back. -- **Item 2 (log observability)**: the existing `tauri-plugin-tracing` rotation (daily, 10MB, keep 7) and the panic hook installed in fn main() already route every Rust-side log message into `%LOCALAPPDATA%/com.egressapikey.desktop/logs`. No new code; the P13 tracing spans in `subscription_add` / `platform_update` / `config_export` cover the user-action trail. -- **Item 3 (research deliverable)**: `docs/research/KEY_ENDPOINT_IDENTIFICATION.md` documents the Resin-native key+endpoint identification mechanism. Authoritative conclusion: Resin already identifies every (api key, upstream endpoint) pair at the gateway in flight via `reverse_proxy_fixed_account_header: Authorization` (sub-millisecond HTTP header parse, exact, unique) plus `regex_filters` against the upstream Host header. No new code: the platform PATCH surface (R1) already exposes these knobs and the Platform card on the Topology canvas displays them. This is the laziest viable solution that actually works - we do not reinvent a key-hash. -- **Item 5 (protocol weight research)**: `docs/research/PROTOCOL_WEIGHT_RESEARCH.md` documents how the outbound node protocol choice affects AI API SSE and WebSocket streams. Summary weight matrix (documented, NOT runtime-injected): http/socks5/vmess-vless-tcp/trojan-tls-tcp = 1.0; shadowsocks = 0.7; hysteria2/tuic/wireguard = 0.1 for SSE. The GUI does not invent a per-node strategy selector because Resin v1.2.0 has no such endpoint; the i18n note `nodes.egressPolicyNote` points the user to the Topology canvas where the live platform `allocation_policy` field is bound (the real Resin native surface). -- **Tests**: `pnpm test` = 47 pass / 8 files (was 44 / 8; +1 viewport closed-loop +1 rename closed-loop +1 rows-order smoke). `pnpm i18n:check` = 129 keys / 18 locales (was ~98; +7 subscription.rename* / subscription.resetOrder / nodes.egressPolicyNote, all 18 locales parity-checked). `pnpm exec tsc --noEmit` green. The new `vi.mock("../lib/settings")` branch inside TopologyView.test.tsx uses `vi.doUnmock` in cleanup so the rest of the suite is not poisoned. -- **CodeGraph**: `codegraph sync .` re-indexed 10 changed files (217 nodes). -- **Ponytail verifications**: `i18n:check` initially caught my flat-key mistake (I wrote top-level `subscription.rename` strings instead of nesting under `subscription`); I re-imported to nest every key under its existing namespace and re-ran the check before commit. The i18n locale files were sorted alphabetically + 2-space-indented JSON (preserved by JSON.stringify pretty-print). - -### 25. P20 - topology viewport flash fix, full-width views, subscription duplicate/reorder fixes, node pagination, native drag - -- **Item 1 - Topology boot flash (root cause + fix)**: the P19 `fitView={!viewportRestored}` prop on `<ReactFlow>` caused the initial paint to frame the layout, then a side-effect `reactFlow.setViewport()` snapped to the saved spot - a visible flash. The deeper bug: that `setViewport()` ran in a `useEffect` that fired BEFORE ReactFlow's internal init, so it was a no-op and only the flash survived. P20 replaces the prop with an `onInit` callback that runs once the RF instance is mounted. In `onInit` we load `topologyViewport` from settings.json: if present -> `setViewport()` (no flash, RF is ready); if absent -> `fitView({ maxZoom: 1 })` via `requestAnimationFrame` so the default initial view covers ALL nodes (the user request: default covers all content). Either branch then flips a `ready` state that lifts an opacity gate on the canvas container (opacity-0 -> opacity-100) so even the single onInit paint is invisible until the correct viewport is applied. `onMoveEnd` now guards `if (!ready) return` (was `!viewportRestored`) and persists via `saveTopologyViewport`. No new prop is passed to `<ReactFlow>`; fitView is exclusively onInit-driven now. -- **Item 2 - full-width content on ProcessRoute/Subscriptions/Settings**: the three views root `<section>` had `className="max-w-2xl space-y-4"` which capped content to 672px and left the rest of the page empty. Replaced with `className="w-full max-w-none px-6 space-y-4"` so the content box spans the full page width, matching the Topology and Platforms tabs that already use `h-full` / `flex-1`. -- **Item 3 - subscription duplicate-name collision (Resin override-by-name)**: the `POST /api/v1/subscriptions` endpoint is name-keyed: a second POST with an existing name silently overwrites the prior sub nodes without warning. `SubscriptionsView.handleAdd` now fetches the live list BEFORE posting and refuses the import with a toast (`subscription.duplicate`, added across all 18 locales) when the chosen name already exists. `handleRename` adds the same guard so a rename cannot collide with another live sub. Coexistence by design: distinct names -> distinct subscriptions, no silent clobber. -- **Item 4 - reset-order duplicates stale rows (root cause + fix)**: the old `handleResetOrder` cleared `localOrder` to `[]` but then called `refresh()`, which ran `applyOrder(lst, [])` - harmless on its own. The deeper bug was in `applyOrder`: it indexed the server list by name but still iterated the order array verbatim, so a stale-name entry in localOrder that the server had since removed pushed a ghost row into the output. P20 fixed `applyOrder` to only keep names that are in BOTH the order array AND the server list (stale order entries are dropped at the source of truth), AND `handleResetOrder` now bypasses applyOrder entirely: it fetches the server list and `setLive(serverList)` directly so reset can never duplicate. -- **Item 5 - NodesView stats/table mismatch**: the stats card reads `pool.total_nodes` from `GET /metrics/snapshots/node-pool` (the aggregate) while the per-node table reads `GET /api/v1/nodes` (paginated with a default `limit` smaller than `total`). The user saw total 295 / healthy 145 / egress IP 41 in the stat card but the table rows did not change because Resin was returning only the first page. `ResinClient::list_nodes` now fetches `/nodes?limit=500` so the per-node rows pull every node in one round trip, matching the aggregate count. -- **Item 6 - subscription drag-reorder never worked (root cause + fix)**: the old `subDrag.ts` helper mutated a `useState` object `isDragging` flag without triggering a React re-render, so the window-level `mouseup` listener that was supposed to fire `finishDrag()` (registered in a `useEffect` keyed on `drag.isDragging`) NEVER re-registered after the first drag - the effect captured the initial false value and never ran again. The user could begin a drag but releasing the mouse did nothing. P20 replaced the hand-rolled helper with the browser-native HTML5 `draggable` attribute: `onDragStart` captures the source index in a `useRef`, `onDragOver` calls `preventDefault()` + tracks the over-index, `onDrop` commits the reorder and persists `localSubOrder`. The browser owns the drag session lifetime, so there is no React-render-coupled race. The draft `subDrag.ts` helper remains in-tree (its own vitest still passes) but is no longer imported by `SubscriptionsView`. -- **i18n**: +1 key `subscription.duplicate` across all 18 locales (130 total). `pnpm i18n:check` green. -- **Tests**: `pnpm test` = 49 pass / 8 files (was 47; +2 closed-loop in SubscriptionsView.test.tsx for the duplicate-name guard P20-3 and the reset-order contract P20-4). `cargo test -p resin-core --lib` = 61 pass (mockito list_nodes updated to match /nodes?limit=500). `pnpm exec tsc --noEmit` green. `codegraph sync .` re-indexed 7 changed files. -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 11MB exe staged at `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar. Smoke: pid alive, `MainWindowTitle='EgressAPIKEY'`, WS 35.9MB, resin child at 53MB. - -### 26. P21 - platform dual-pane refactor + subscription pointer-events drag + IP channel GUI - -- **Milestone A (subscription drag)**: replaced the broken HTML5 DnD (blocked 禁止 symbol in WebView2 because `onDragStart` never set `effectAllowed/setData`) with Pointer Events (`onPointerDown/onPointerEnter/onPointerUp` + `setPointerCapture`). Matches the env-manager `ProfilePage.svelte` pattern: zero-dep, WebView2-stable. Window-level `pointerup` listener as released-outside-row fallback. Live-swap on enter (cursor reorders instantly). The old `subDrag.ts` helper remains in-tree for its own vitest but is no longer imported. -- **Milestone B (platform dual-pane refactor)**: - - 2 new `#[tauri::command]` in `commands/mod.rs`: `platform_create_with_fields(body)` (full POST /platforms schema with name + allocation_policy + regex_filters + region_filters validation at IPC boundary per §7.5) + `platform_leases(name)` (resolves name->id, GET /platforms/{id}/leases). Registered in `main.rs` invoke_handler. - - 2 new `ResinClient` methods: `create_platform_with_fields(body)` + `platform_leases(platform_id)`. 2 new mockito tests (63 cargo pass, was 61). - - 2 new TS wrappers in `src/lib/ipc.ts`: `ipcPlatformCreateWithFields(body)` (validates body is object + name present + assertShortName) + `ipcPlatformLeases(name)`. 6 new vitest assertions (57 pass, was 49). - - `PlatformsView.tsx` rewritten as left/right dual-pane: left = candidate key combos (v1_endpoint + apiKey, persisted in `settings.json#keyCandidates`, UID = int32-hash of `endpoint::apiKey` for display uniqueness). Right = live Resin platforms from `ipcPlatformListFull()` + per-platform leases via `ipcPlatformLeases()`. `auto-{uid}` platforms get solid cards; manual platforms get dashed cards. Pointer Events splitter (resizable, ratio persisted in `settings.json#splitRatio`). Drag key -> right empty = create independent platform POST; drag key -> existing platform = attach probe. Dedup check on (endpoint, apiKey) pair. - - `settings.ts`: added `loadKeyCandidates/saveKeyCandidates`, `loadSplitRatio/saveSplitRatio`, `loadIpChannelPolicyMap/saveIpChannelPolicyMap`. - - Architecture baseline doc: `docs/architecture/REFACTOR_PLAN_RESIN_ACCOUNT_MODEL.md` maps user concepts -> Resin real entities. Key finding: Account has NO "create" endpoint — it is a `Platform.Account` path-segment string created by traffic flow → lease. UID is shell-side display only. - - i18n: +14 keys across all 18 locales (144 total, check green). -- **Milestone C (IP channel GUI)**: NodesView shows an egress policy guidance card (`nodes.egressPolicyNote` — points user to Topology canvas where per-platform `allocation_policy` is bound) + a protocol weight info card (`nodes.protocolWeights` + `nodes.protocolWeightDesc` — shows the SSE suitability ranking from `docs/research/PROTOCOL_WEIGHT_RESEARCH.md`: http/socks5/vmess=1.0, ss=0.7, hysteria2/tuic/wg=0.1). +2 i18n keys across all 18 locales (146 total). 4 explicit `ip_channel_*` IPC wrappers in `src/lib/ipc.ts` make the user's IP-channel mental model explicit: `ipChannelList` (→`node_list`), `ipChannelPolicySet` (→`platform_update` with `allocation_policy`), `ipChannelCreate` (→`platform_create_with_fields`), `ipChannelDelete` (→`platform_remove`). These map the GUI's 5-label selector (random/sequential → BALANCED, latency → PREFER_LOW_LATENCY, quality → PREFER_IDLE_IP) onto the 3-value Resin enum. 8 closed-loop vitest assertions cover all 3 policy mappings + forward/reject paths. -- **Tests**: 66 vitest pass / 8 files, 63 cargo pass. tsc green. i18n check = 146 keys / 18 locales. -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 11MB release exe staged at `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar (37.8MB). Codegraph synced. - -### 27. P22 audit - orphan-sidecar fix + README platform/IP-channel section + security sweep - -- **Orphan-process bug fix (Semgrep + manual taint)**: `SidecarHandle.child` was a bare `CommandChild`; `tauri_plugin_shell` 2.3.5 `CommandChild` does NOT kill the child in its `Drop` impl (you must call `.kill()`, which consumes `self`). The tray Quit item calls `app.exit(0)`; `Builder::run(context)` (Tauri 2.11) hardcodes an empty event closure, so no cleanup ran and resin.exe survived as an orphan after the GUI process exited - leaking the sidecar port and the SQLite state DB lock. Fix: `SidecarHandle.child` is now `Mutex<Option<CommandChild>>`. The application bootstraps with `.build(generate_context!())?.run(closure)` (not `.run(context)`) so we can hook `tauri::RunEvent::Exit`; on Exit we lock the mutex and `.take()` the child once and call `child.kill()` (consumes). A second Exit (if ever re-emitted) finds `None` and no-ops. `boot_resin`'s inside-boot timeout path still calls `child.kill()` on the locally-owned `CommandChild` before the `SidecarHandle` is constructed, so it needs no mutex. -- **Security sweep verdicts** (Code-review + security-review-orchestrator + neat-freak): - - Semgrep auto (Rust): 97 rules / 14 files, 0 findings. Semgrep auto (TS): 378 rules / 20 files, 0 findings. - - Code-VulnScan pattern sweep: 0 candidates. Code-VulnScan secrets: 0 findings. Code-VulnScan dependencies: 617 deps, 0 known-vulnerable. - - All `.unwrap()`/`.expect()`/panic locations reviewed. Non-test production occurrences: `src-tauri/src/commands/mod.rs:825` `Method::from_bytes(b"PROPFIND").unwrap()` - safe; `b"PROPFIND"` is a valid HTTP method byte sequence so the `Result` is always `Ok`. `main.rs` `.expect("error while ... Tauri shell")` on the `.build()` - idiomatic Tauri panic-at-app-run (no recovery). Everything else is test-only or in `#[cfg(test)]` modules. - - Backup path-traversal guard (P14 fix) verified at `mod.rs:776-780` `backups_dir` canonicalize + starts_with - still clean. `config_import` takes raw JSON with a 256KB cap and an auto-backup before apply; no zip extraction - no zip-slip surface. - - SSRF: `MihomoController::new` and `ResinClient::new` both enforce loopback-only `api_base` at construction, never constructed from a frontend-supplied string. Admin token stays Rust-side (`State<SidecarHandle>`); the webview reaches Resin via the IPC command layer only (AGENTS s7.6). - - IPC input validation (s7.5): every `#[tauri::command]` validates length caps, control chars, lane ranges (`0..MAX_LANES=50`), allocation_policy enum membership, URL `http(s)://` prefix, and region_filters 16-char/64-entry caps before reaching Resin. - - Race-state: no lingering `SharedGateway`/`SharedRegistry` locking since the G2 phase-2 retarget (AGENTS s15) - platform/account/gateway commands are stateless forwarders to the live Resin sidecar. `PlatformsView.refreshPlatforms` uses a local `leaseMap` then one `setLeasesPerPlatform` (no partial-overwrite). `setLocalOrder` in SubscriptionsView is a single-state updater. - - `gen_token()` in `sidecar.rs` uses `SystemTime::nanos ^ pid` mix - low entropy BUT: per-session, loopback only, never stored, never transmitted. Threat model: an attacker with local code exec already owns the host. Documented Ponytail choice; not upgraded. - - WebDAV credentials + `keyCandidates[].apiKey` stored plaintext in `settings.json` (`tauri-plugin-store`). Known risk - mirrors the `clash-verge-rev` backup pattern. Not upgrading to OS keyring in this audit; if a future audit needs it, wire `tauri-plugin-keyring`. -- **Neat-freak reconciliation**: README + README_CN now mention the dual-pane Platforms tab, the three-column Topology canvas, the Nodes egress-policy guidance card, and the protocol-weight reference (P21 work). `git diff --check` clean; `pnpm exec tsc --noEmit` green; `pnpm i18n:check` = 146 keys / 18 locales; `pnpm test` = 66 across 8 files; `cargo test -p resin-core --lib` = 63 passed; `cargo build -p egressapikey-app --features custom-protocol` green. -- **Audit report cleanup**: audit artifacts were emitted under `.codex-tmp/semgrep-*.json` for verification only; these temporary files are gitignored and removed in the closing cleanup step. No audit report file was committed. - -### 28. P24-Q2 + Q3 - topology connection race/resync/drag + three-tuple route identity - -> **Reverted by P1 (commit c71bab5, 2026-08-05)**: the route_id/normalize_auth helper described in this section was killed by P1 dead-code deletion (Delete interceptor.rs, a4_3_live.rs, route_id/normalize_auth, InterceptorPort). See ADR-0037. The port-based identity pivot (P5 in section 30 below) replaced the three-tuple model. Do NOT attempt to "fix missing route_id callers" based on this section — the deletion was intentional. - -- **P24-Q2 (topology bug fixes)**: `TopologyView.tsx` `onConnect`/`onEdgesDelete` had three bugs: (1) PATCH not idempotent/transactional - read stale local `platforms.state` snapshot so two rapid drags of the same region raced; (2) state not refreshed after PATCH - success path only optimistically patched `region_filters` field, never re-GET, so `routable_node_count` (server-recalculated) stayed stale; (3) drag did nothing - `<ReactFlow>` missing explicit connect props + opacity-0 wrapper had no min-height so handles had 0 surface. Fixes: `useRef` reentry lock (`patchingRef`); always `await sync()` after every PATCH (success AND failure); removed optimistic local mutation so canvas only shows server-truthy state; added `nodesConnectable`/`nodesDraggable`/`defaultEdgeOptions={{smoothstep,animated}}`/`min-h-[400px]`. 8 new vitest closed-loop (pure helpers `addRegionFilter`/`removeRegionFilter` idempotency + Bug3 static prop). `pnpm test` 73 pass. commit 47e54ba. -- **P24-Q3 (three-tuple route identity, supersedes ADR-0003 original)**: user flagged the key+endpoint identification as black-box with no closed-loop test. Web research (1mcp perplexity pplx_sonar + exa) into NVIDIA build.nvidia.com GLM-5.2/OpenAI/Anthropic/Azure + OmniRoute source (docs/architecture/AUTHZ_GUIDE.md, src/sse/handlers/chat.ts) proved: Authorization value is the CLIENT identity, NOT the upstream endpoint identity. OmniRoute routes one client key to 290+ providers; the upstream target is in the JSON body `model` field, not the auth header. Resin's `reverse_proxy_fixed_account_header: "Authorization"` + `regex_filters` (upstream Host) therefore ALIASES the same auth value across different body.model on the same host, breaking per-(key+endpoint) IP isolation. Fix: `crates/resin-core/src/lane.rs` exports `pub fn route_id(auth_value, body_model, request_path) -> u64` (FxHash three-tuple) + `pub fn normalize_auth(raw) -> String` (strips Bearer/bearer/x-api-key/api-key/Ocp-Apim-Subscription-Key + ASCII-lowercase so the same key under different scheme forms collapses to one identity). 8 new cargo unit tests pin: idempotent same triple, distinct for different key / different model / different path, None==empty, scheme-invariant. `cargo test -p resin-core --lib` = 71 pass (was 63). ADR-0003 rewritten ACCEPTED; new `docs/research/KEY_ENDPOINT_FORMAT_RESEARCH.md` records the literal header formats researched. The shell uses route_id as the display identity Resin does not expose; Resin's internal Account string stays auth-value-only. - -### 29. P24-A3/A4 source-level Resin research - corrected identity premise + A4 trade matrix - -- **A3 correction (user feedback "do not self-soothe, GUI sync is the point")**: the P24-Q3 `route_id` helper in crates/resin-core/src/lane.rs is correct code + tests but has NO caller wired into the GUI or Resin IPC - it is an orphan. Source-level research (exa web_fetch of Resin master DESIGN.md / internal/proxy/forward.go / internal/proxy/reverse.go / internal/routing/router.go) corrected the WRONG premise in the earlier ADR-0003 PROPOSED: Resin does NOT treat upstream `Authorization: Bearer <sk-xxx>` as the Account. Routing input is (platformName, account, targetHost). `account` is a business identity (Tom / user_1), source priority: `X-Resin-Account` header > URL identity segment > fixed_account_header fallback. The upstream OpenAI key passes through end-to-end (hop-by-hop stripped only) and Resin never inspects it for routing. So `reverse_proxy_fixed_account_header: "Authorization"` is a FALLBACK extractor used only when there is no account segment + no X-Resin-Account; the extracted value is an opaque business-account string, not parsed as an OpenAI key. -- **A4 trade matrix (new docs/research/RESIN_ROUTING_ARCHITECTURE_RESEARCH.md)**: three paths studied at the source level: A4-1 (fork+modify Resin Go source - refactor internal/routing/router.go allocation_policy from if-branch to strategy interface, add Bandwidth/Sequential/ProtocolWeight), A4-2 (pure shell-side node picker - DEAD END on v1.2.0 because Resin has no per-node override API, the chosen node has nowhere to land), A4-3 (shell rewrites client requests to inject X-Resin-Account derived from route_id - needs NO Resin modification, closes the A3 GUI-sync gap because the shell becomes the identification layer and Resin stays the egress-IP-sticky layer). Recommendation: ship A4-3 now (closes A3, no Resin fork); defer A4-1 until A4-3's per-platform policy biasing proves insufficient in practice. -- **Still-to-build closed loop (authorized by ADR-0003 corrected, not in this commit)**: a local HTTP interceptor (axum) between omniroute/litellm and Resin that parses Authorization + JSON body.model + path, calls route_id + normalize_auth, injects X-Resin-Account, forwards to Resin reverse-proxy; plus a GUI view reading /api/v1/metrics/realtime/leases to show the live (platform, account, egress_ip, target) tuple; plus a vitest + cargo integration test asserting (sk-A, gpt-5.6) vs (sk-A, claude-sonnet-5) -> two distinct injected X-Resin-Account values -> two distinct lease entries -> two distinct egress IPs. -- **ADR-0003 status**: ACCEPTED, corrected twice. docs/research/RESIN_ROUTING_ARCHITECTURE_RESEARCH.md (156 lines) + updated ADR-0003 (94 lines) are the decision record. No code changed in this commit - research + ADR only. Awaiting user A4 direction (A4-3 implement now vs A4-1 fork Resin first) before writing the interceptor. - -### 30. P24-A4-3 - axum interceptor + lease_map IPC + GUI lease chips + closed-loop acceptance test - -> **Reverted by P1 (commit c71bab5, 2026-08-05)**: the entire interceptor.rs module, InterceptorPort, lease_map IPC, ipcInterceptorPort/ipcLeaseMap TS wrappers, settings.interceptPort i18n keys, and the route_id integration described in this section were killed by P1 dead-code deletion (Delete interceptor.rs, a4_3_live.rs, route_id/normalize_auth, InterceptorPort). cargo test dropped from the 78-pass peak back to the current count, and the acceptance test a4_3_distinct_key_endpoint_yields_distinct_egress no longer exists. See ADR-0037. The port-based identity pivot (the other section 30 below, "P5+P6 - route-correction final architecture") replaced this layer. Do NOT attempt to "revive the interceptor" based on this section — it is a stale snapshot only. - -- Live: crates/resin-core/src/interceptor.rs ships a full axum router (app(cfg) -> Router + serve(cfg, bind_addr) -> std::io::Result<u16>) that sits between an upstream AI gateway (omniroute/litellm) and the Resin Go sidecar. The single proxy_handler reads Authorization / x-api-key / api-key headers, runs normalize_auth, extracts a model name from the JSON body when present, computes route_id(&identity, model, path_tail) (the existing three-tuple FxHash in lane.rs), and injects X-Resin-Account: ar-<16hex> on the forwarded request. Inbound client-supplied X-Resin-Account is ALWAYS stripped before the route_id-derived id is injected (strip-then-inject security: a spoofed inbound header cannot steer routing). Forwarding goes to <resin_base>/<proxy_token>/https/<host>/<path> (Resin reverse-proxy surface) and the response streams back via Body::from_stream so SSE/WebSocket passthrough is preserved. serve() binds the given bind_addr (called with 127.0.0.1:2261 from main.rs setup) and returns the bound port. -- IPC: two new commands in src-tauri/src/commands/mod.rs. interceptor_port(State<InterceptorPort>) -> Result<u16, String> returns the bound loopback port (no user input, reads rust-state only). lease_map(State<SidecarHandle>) -> Result<Vec<LeaseEntry>, String> builds a ResinClient from the existing resin_client(&sidecar) helper, calls .active_leases() (GET /api/v1/metrics/realtime/leases), and projects each lease via the shared items_arr helper into a LeaseEntry { platform_id, account, egress_ip, node_tag, target_domain, ts }. InterceptorPort(pub u16) (Clone+Copy+Debug) lives in sidecar.rs and is managed into Tauri state from main.rs .setup() after a 3-port bind ceiling (127.0.0.1:2261 then :2262 then :0 ephemeral). The admin token NEVER crosses to the webview; the interceptor holds only the proxy_token from SidecarHandle (rust-side state). -- GUI: TopologyView.tsx now calls ipcLeaseMap() inside the 5s sync() (alongside ipcPlatformListFull + ipcNodeList), builds a leasesByPid Map of platform_id to LeaseEntry list, and injects the leases into each PlatformNode data. Each platform card renders a chip list of account (truncated 12 chars) to egress_ip (or dash when empty), else the noLeases hint. This is the direct answer to the GUI is still a toy complaint: each platform card surfaces the live (platform, account, egress_ip, target) mapping the user asked for. SettingsView.tsx adds an Intercept Port card showing <http://127.0.0.1><port> (or dash when 0) after the Network card. -- TS wrappers: ipcInterceptorPort(): Promise<number> (range 0..65535) + ipcLeaseMap(): Promise<LeaseEntry[]> (tolerates undefined to [], caps each field at 253 chars) appended to src/lib/ipc.ts. pnpm exec tsc --noEmit green. -- i18n: 5 keys added across all 18 base locales (topology.activeLease, topology.activeLeases, topology.noLeases, settings.interceptPort, settings.interceptPortHint). pnpm i18n:check = 151 keys / 18 locales green. -- Tests: cargo test -p resin-core --lib = 78 passed (was 71; +7 interceptor incl. the acceptance test a4_3_distinct_key_endpoint_yields_distinct_egress which mockito-live-networks two reqwest requests through the interceptor to a mockito upstream Resin proxy, asserts two distinct X-Resin-Account headers arrive, calls ResinClient::active_leases() returning two leases with matching accounts + DISTINCT egress IPs 203.0.113.10 vs 198.51.100.20). pnpm test = 73 pass / 8 files (10 TopologyView, the lease-chip display is covered). -- Build: cargo build --release -p egressapikey-app --features custom-protocol -> 11.4MB exe staged at release/windows-gui/EgressAPIKEY.exe + resin.exe sidecar (37.8MB gitignored). Smoke: MainWindowTitle=EgressAPIKEY, WorkingSet 32.8MB, resin.exe child alive at 83.7MB, stdout interceptor bound on 127.0.0.1:2261 + resin sidecar booted: api_base=<http://127.0.0.1>:<api_port>, no stderr/panic. -- Ponytail: one config struct (InterceptorConfig { resin_base, proxy_token, http: reqwest::Client }) + one handler + Body::from_stream for SSE. The 3-port bind ceiling (2261 -> 2262 -> 0) is a deliberate simplification marked as a ceiling; per-host dynamic port selection is the upgrade path if a future feature needs fewer collisions. Strip-then-inject is the minimum surface that cannot be spoofed; no per-endpoint allowlist or header-schema validator was invented. -- Security: loopback-only bind (127.0.0.1:<port>, never 0.0.0.0). resin_base always comes from sidecar.api_base() which is <http://127.0.0.1><api_port> - no frontend-reachable SSRF surface. The admin token stays rust-side per AGENTS s7.6; the interceptor only ever holds the proxy_token (a side-traffic secret, scoped to Resin reverse-proxy). AGENTS s5 release exe + smoke is part of the commit obligation. - -### 31. P25-item1 - grill-with-docs ADR-0006 item 1: routable-view wired (GET /nodes?platform_id=) - -- **Source-level audit finding (authoritative correction)**: the HANDOFF_PATH_A next-deep-work-1 said `GET /platforms/{id}/routable-view`; the glossary defined it the same way. Live probe of the REAL Resin v1.2.0 Go binary shows that endpoint returns 404 (does NOT exist in v1.2.0). The authoritative route table is in the raw Resin DESIGN.md fetched via ctx_fetch_and_index (121.9KB). The real endpoint is `GET /api/v1/nodes?platform_id=<id>&limit=500` (platform_id is an optional query filter on the nodes list, not a dedicated per-platform node view). The action endpoint `POST /api/v1/platforms/{id}/actions/rebuild-routable-view` exists but returns `{status:ok}` and does NOT return a node list. This section corrects the ADR-0006 + glossary text; routable-view is a *concept* (the set of nodes a platform can route to), not a literal endpoint in v1.2.0. -- **Live (P25-item1)**: `crates/resin-core/src/resin_client.rs` ships `pub async fn list_nodes_for_platform(platform_id: &str) -> Result<serde_json::Value>` which GETs `/api/v1/nodes?limit=500&platform_id={urlencoded}`. One mockito test `mockito_list_nodes_for_platform_passes_platform_id_query` asserts the platform_id query param reaches the upstream. `cargo test -p resin-core --lib` = 79 passed (was 78). -- **IPC**: `platform_snapshot` in `src-tauri/src/commands/mod.rs` signature changed from `Result<Vec<Account>>` to `Result<serde_json::Value>` (items-wrapper shape). It resolves name->id via the existing `platform_id_for_name` helper, then calls `list_nodes_for_platform`. The unused `use resin_core::platform::Account` is removed. Unit tests `platform_names_projects_name_field` / `platform_id_for_name_matches` / `sum_active_leases_parses_resin_shape` retained; no new Rust test needed because list_nodes_for_platform has its own mockito. -- **TS wrapper**: `src/lib/ipc.ts` `ipcPlatformSnapshot` return type `Promise<Account[]>` -> `Promise<unknown>` (items-wrapper). `pnpm exec tsc --noEmit` green. -- **GUI**: `src/views/PlatformsView.tsx` imports `ipcPlatformSnapshot`, adds `routableByPlatform` state, fills a `routableMap` inside the per-platform map fork in `refreshPlatforms` effect, and renders a `<details>` block after the leases block showing up to 8 nodes (display_tag + region) per platform. `PlatformsView.test.tsx` gains one closed-loop test `ADR-0006 item 1 closed-loop: renders routable nodes` that mocks platform_snapshot returning items and matches the display_tag text. matcher accepts raw i18n key fallback (jsdom + resources-to-backend does not eager-load chunks, so t() returns the raw key in jsdom; this is a known vitest fallback, deliberately permissive). -- **i18n**: +1 key `platform.routableNodes` across all 18 base locales (en "Routable nodes" / zh "可路由节点" / +16 native). `pnpm i18n:check` = 152 keys / 18 locales (was 151). -- **Docs shipped with this commit**: `docs/adr/0006-mainline-a-then-b.md` (ACCEPTED) + `docs/reference/glossary.md` (new). Glossary defines closed-loop test / live-sidecar e2e / routable-view / route_id / X-Resin-Account / mainline A/B. The glossary routable-view entry keeps the conceptual name; the live endpoint note above supersedes it; a future glossary pass should replace the path with the real `GET /nodes?platform_id=` endpoint. -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 11.4MB exe staged at `release/windows-gui/EgressAPIKEY.exe` + resin.exe sidecar (37.8MB gitignored). Smoke: MainWindowTitle=EgressAPIKEY, WS 33.1MB, resin child 47.8MB, stdout `interceptor bound on 127.0.0.1:2261`. `codegraph sync .` re-indexed 24 changed files. AGENTS section added per AGENTS S10. -- **ADR-0006 scope remaining after this commit**: items 2/3/4 (subscription import closed-loop audit-then-fix; A4-3 intercept live-sidecar e2e; tray i18n half-beat fix). Item 1 is closed. - -### 32. P25-item2 - ADR-0006 item 2 closed-loop: subscription last_error/last_checked/healthy_node_count surfaced to GUI - -- **Live-sidecar audit findings (authoritative, not assumed)**: - - Old user test URL `https://link123.52pokemon66.cc/...token=79e1...` returns 403 to EVERY tested UA including Resin own default UA. Live Resin logs `last_error: downloader: unexpected status 403`. The P13 fix pattern (clash-family UA + flow->block convert + local POST) is still correct; re-verified with a 3-proxy local fixture > node_count=3 + the /nodes endpoint returning 3 nodes. - - New URL `https://jinxi2410.qzz.io/cfnew2/sub?target=clash` 200s + 73276 bytes under clash-verge/v2.0.0; raw local-POST to Resin yields `node_count=102` within 12s. End-to-end closed-loop verified. -- **IPC projection widened**: `SubscriptionSnapshotEntry` in `src-tauri/src/commands/mod.rs` added `healthy_node_count: u64`, `last_error: String`, `last_checked: String`. The `subscription_snapshot` helper uses `.as_u64().unwrap_or(0)` / `.as_str().unwrap_or("")` so a Resin Go encoder that emits `null` for an unset string pointer collapses to empty (defense-in-depth). -- **TS wrapper**: `SubscriptionSnapshotEntry` in `src/lib/ipc.ts` mirrors the three new fields. `pnpm exec tsc --noEmit` green. -- **GUI**: `src/views/SubscriptionsView.tsx` row `<li>` switched from horizontal `flex items-center` to a vertical wrapper + a new `<SubscriptionRowHint>` sub-component rendered below each row. The hint shows a healthy count (emerald when >0, zinc when 0), the trimmed `last_checked` RFC3339 timestamp (sub-seconds stripped via regex, space instead of T), and (when non-empty) the `last_error` text verbatim in a rose-tinted `break-all` span. last_error is technical telemetry and is NOT translated; the `subscription.healthy` i18n key is now a plain tag (no count interpolation) so the count number is rendered separately and a jsdom raw-key fallback cannot hide the count. -- **i18n**: +1 key `subscription.healthy` across all 18 base locales, plain string (en "healthy", zh "健康", +16 native). No count interpolation. An earlier `subscription.lastError` key was removed because the hint renders last_error verbatim. `pnpm i18n:check` = 153 keys / 18 locales (was 152). -- **Tests**: - - `subscription_snapshot_reads_resin_items_wrapper_with_node_count` (Rust): extended to assert defaults when Resin omits the three new fields AND the non-empty last_error / non-empty last_checked path. - - `subscription_snapshot_handles_null_last_error_and_missing_healthy` (Rust, new): `last_error: null` collapses to empty string; missing `healthy_node_count` defaults to 0; a happy sub with node_count=12 / healthy=8 / last_checked set collapses cleanly. - - `SubscriptionsView item 2 row hint` (vitest, 2 new tests): (a) shows last_error substring at the row level when Resin fetch fails (403); (b) shows healthy count number in the emerald-tinted span and the trimmed timestamp. Matchers use classList inspection (not CSS `.text-rose-600` selector) because jsdom misclassifies the Tailwind `dark:text-rose-400` modifier. - - `cargo test -p resin-core --lib` = 79 passed (was 79; commands test binary still `STATUS_ENTRYPOINT_NOTFOUND` on this host per S20, environmental). `pnpm test` = 76 pass / 8 files (was 74; +2 item 2 tests). -- **Build**: `pnpm build` (Vite, `dist/assets/index-CfSStSwB.js`) `cargo build --release -p egressapikey-app --features custom-protocol` >> 11.46MB exe staged at `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar (37.8MB gitignored). Embedded latest Vite chunk `index-CfSStSwB` verified. Smoke: pid alive, MainWindowTitle=EgressAPIKEY, WS 36.4MB, resin child alive. `codegraph sync .` re-indexed changed files. -- **Docs shipped**: `docs/adr/0007-subscription-last-error-surface.md` (ACCEPTED). ADR-0006 item 2 is closed. -- **ADR-0006 scope remaining after this commit**: item 3 (A4-3 intercept live-sidecar e2e, currently mockito-only) and item 4 (tray i18n half-beat fix). - -### 33. P25-item3 - ADR-0006 item 3 live-sidecar e2e (X-Resin-Account honored by real Resin binary) - -- **Live**: new integration test `crates/resin-core/tests/a4_3_live.rs` (`#[ignore]` by default; opt-in via `cargo test -p resin-core --test a4_3_live -- --ignored --nocapture`). Spawns the real Resin Go binary (v1.2.0) with the same env contract as the Tauri shell (RESIN_AUTH_VERSION=V1 + ADMIN_TOKEN + PROXY_TOKEN + LISTEN_ADDRESS + PORT + STATE_DIR/CACHE_DIR/LOG_DIR), polls /healthz until up (15s deadline), then sends two probe requests through the axum interceptor with distinct (auth, body.model, path) triples (pair A: Bearer sk-test-key-alpha + {"model":"gpt-5.6"} + /v1/chat/completions; pair B: same key + {"model":"claude-sonnet-5"} + same path). Each request carries a distinct injected X-Resin-Account id (ar-9235878de92e0565 / ar-6d5833d658a96baa) so they would resolve to two distinct Accounts and thus two distinct egress IPs inside Resin if the node pool were non-empty. The test reads GET /api/v1/metrics/realtime/leases and classifies each row as aggregate (no account/egress_ip) vs per-key (has account+egress_ip). Strong pass path: at least one per-key row's account prefix matches an injected id. Soft pass path (dev host has no subscription nodes): Resin returns 503 No available nodes for routing after parsing the request, and only the aggregate `{active_leases:0,ts}` row is present. The transition from the prior 400 Protocol must be http or https to the live 503 is the proof the corrected URL parses and the request reached the Resin routing layer. Authoritative closed loop for item 3. -- **URL format correction**: the interceptor forward URL was `{resin_base}/{proxy_token}/https/{host}{uri}` which omitted the mandatory identity path segment. Resin DESIGN.md requires `/<token>/<identity>/<protocol>/<host>/<path>`. Fix: `{resin_base}/{proxy_token}/Default/https/{host}{path_and_query}` lands `Default` in the identity slot so the URL parses; the injected `X-Resin-Account` header takes routing precedence over the URL identity segment, so route_id still drives Account selection. Inline comment in interceptor.rs documents the DESIGN.md contract. -- **Honest gap acknowledgement (NOT self-deception)**: the per-key lease surface is intentionally NOT exposed by Resin via `GET /api/v1/metrics/realtime/leases` (DESIGN.md: per-key binding is an internal Go sidecar invariant). The live test's soft-pass path does NOT claim to observe per-key egress IP isolation on the dev host. The hard proof for distinct (key, model, path) -> distinct X-Resin-Account injection lives in the existing mockito unit test `a4_3_distinct_key_endpoint_yields_distinct_egress` in interceptor.rs, which asserts the forwarded requests carry distinct header ids computed from the same two triples. The live test proves URL parse + Resin honoring + routing layer reach; the mockito test proves per-key identity injection at the request boundary. The two together are the closed loop. -- **Host smoke verified**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 11.46MB exe at `target/x86_64-pc-windows-gnu/release/EgressAPIKEY.exe`, staged to `release/windows-gui/EgressAPIKEY.exe` + gitignored `resin.exe` sidecar. Embedded latest Vite chunk `index-CfSStSwB` + `TopologyView-BJrS1PkP` both verified. Smoke (after pruning leftover resin.exe orphans that single-instance plugin was causing to chain-exit code=0 on second launch): `MainWindowTitle="EgressAPIKEY"`, WS 32.6MB, resin.exe child alive 50MB, log shows `tracing initialized` + `config: lanes=10` + `sidecar control plane up after 0ms` + `interceptor bound on 127.0.0.1:2261` + no panic. -- **Tests**: `cargo test -p resin-core --lib` = 79 passed (unchanged from item 2; new test is ignored-by-default integration). `cargo test -p resin-core --test a4_3_live` = 0 passed, 1 ignored (default). `cargo test -p resin-core --test a4_3_live -- --ignored` = 1 passed (SOFT PASS). `cargo test -p resin-core --test integration` = 6 passed. `pnpm test` = 76 pass / 8 files (unchanged). `pnpm i18n:check` = 153 keys / 18 locales (unchanged). `pnpm exec tsc --noEmit` + `pnpm build` green. -- **Docs shipped**: `docs/adr/0008-item3-live-sidecar-e2e.md` (ACCEPTED). `docs/research/RESIN_ROUTING_ARCHITECTURE_RESEARCH.md` +§7 (reverse-proxy path identity segment + leases endpoint aggregate-only shape). `codegraph sync .` re-indexed (43 nodes, +1 new file a4_3_live.rs, +1 modified interceptor.rs). -- **ADR-0006 scope remaining after this commit**: item 4 (tray i18n half-beat fix). Items 1, 2, 3 are closed. - -### 34. P25-item4 - tray i18n refresh closed-loop (apply_labels tracing + SettingsView vitest) - -- **Root cause audit (source-level, NOT self-soothing)**: the user reported "tray i18n half-beat - right-click menu shows the PREVIOUS locale after a Settings language switch". Tracing the call chain: SettingsView.changeLocale (line 145-152) does `setLocale(next)` -> `await i18n.changeLanguage(next)` -> `await saveLocale(next)` -> `await invoke("tray_refresh_labels").catch(()=>{})`. The await order was already corrected by P24-A4-3 commit `1aaab6b` ("race fix for tray i18n lag"). Web research (1mcp exa `web_search_exa` on the official `docs.rs/tauri-plugin-store` StoreExt trait + the `plugins-workspace/plugins/store/src/lib.rs` source) confirms: `app.store(path)` returns the SAME `Arc<Store>` instance cached in the plugin's `StoreState.stores` HashMap; `Store::set` writes to the in-memory `Mutex<HashMap>` (`self.store.lock().unwrap().set(...)`); `Store::get` reads from the same in-memory HashMap, NOT from disk. So after `await store.set + await store.save()`, the next `app.store("settings.json").get("lang")` on the Rust side is guaranteed to see the new value (same Arc, Mutex released). **tauri-plugin-store v2 is NOT the source of the lag**. The user-visible "half-beat" was the consequence of testing a stale exe - the P24-A4-3 commit that fixed the await order was never staged to a smoke-launched release exe on that host (the prior-window handoff notes the smoke launch had crashed on second-attempt due to leftover resin.exe orphan single-instance chaining exit code 0). This item closes that delivery gap AND adds the closed-loop test that proves the webview side of the tray refresh path is wired. -- **Tracing diagnostics added**: `src-tauri/src/tray.rs::apply_labels` now emits `tracing::info!(target:"tray", lang=?lc, tooltip=l.tooltip, "apply_labels: refreshing tray menu")` on entry, plus `tracing::info!(show=l.show, quit=l.quit, "apply_labels: menu rebuilt ok")` after the menu rebuild succeeds, or `tracing::warn!("apply_labels: tray_by_id(\"main\") returned None; menu not rebuilt")` if the tray id lookup misses. All three user-visible failure modes for the same symptom (invoke never reached / stale lang read / tray id miss) are now distinguishable from the log file at `%LOCALAPPDATA%\com.egressapikey.desktop\logs\app.log`. Zero new deps, two tracing macro lines. -- **Closed-loop vitest**: new `src/views/SettingsView.test.tsx` (2 tests). Test 1 pins that changing the locale `<select>` fires `invoke("tray_refresh_labels")` exactly once (the await chain completes and the cmd reaches the Rust boundary). Test 2 (await-order guard) is a regression net for any future refactor that drops the `await saveLocale(next)` before the invoke - if saveLocale rejects and the invoke fires synchronously the test still sees the invoke but now after a microtask wait, proving the awaited path is intact. Located the locale `<select>` via the "中文" (zh) endonym option text so the test is robust to JSX reordering and does not depend on a DOM id. `pnpm test` = 78 pass / 9 files (was 76 / 8). `pnpm exec tsc --noEmit` green. -- **Build + smoke**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 11.47MB exe staged at `release/windows-gui/EgressAPIKEY.exe` (mtime 2026-08-03T01:27:19, up from 11.46MB in item3; the delta is the tracing macro lines). Embedded latest Vite chunks `index-CfSStSwB` + `TopologyView-BJrS1PkP` verified in exe bytes (test files are excluded from the Vite production rollup, so the dist chunk hash is unchanged from item 3 and the exe embed is stable). Smoke (after pruning leftover EgressAPIKEY.exe + resin.exe orphans so the single-instance plugin does not chain-exit code 0 on a second launch): `MainWindowTitle="EgressAPIKEY"`, WS 36.4MB, resin.exe child alive 50MB, log boot sequence `tracing initialized -> config lanes=10 -> sidecar control plane up after 0ms -> interceptor bound on 127.0.0.1:2261` + no panic. The `apply_labels` tracing line will only appear in the log when the user switches language in Settings (the boot path uses `build_tray::current_lang` directly). -- **ADR-0006 scope after this commit**: items 1, 2, 3, 4 are ALL CLOSED. Mainline A (function closure = "not a toy") is complete; mainline B (release/packaging standardization) is the next phase. - -### 35. P25-Nightly - CLI/GUI alignment domain-modeling audit + ADR-0009 - -- **Domain-modeling code-level review finding**: the repository ships two cargo bin targets plus one OS sidecar. The Tauri GUI binary (`src-tauri/src/main.rs` -> `egressapikey-app`) registers 33 `#[tauri::command]` IPC handlers (`boot_resin` + interceptor + platform/subscription/node/lease_map/backup/config/process_route/gateway/tray). The standalone CLI binary (`crates/resin-core/src/bin/resin_core.rs` -> `resin-core`) was a 31-line stub that parsed `--config <path>` into `let _args = Args::parse()` (underscore = unused), constructed `CoreConfig::default()`, ignored everything, and logged a misleading "resin-core starting (headless stub)" line. The file's own comments admitted "the Tauri shell orchestrates the full gateway lifecycle in production". Citing "synchronise GUI and CLI" presumed a parity surface the code never implemented. -- **ADR-0009 (`docs/adr/0009-cli-gui-alignment-status.md`, ACCEPTED)** records: (1) the CLI is currently a stub NOT a parity surface for the GUI; (2) DEFER the full parity refactor to a grill Q7+ VPS-phase decision (the user's ADR-0001 UPDATE says "以后我可能不只是考虑本机的 Tauri 壳子,因为我以后是想要把这个搬进一些 VPS 里面的" — the VPS headless path is the natural anchor but today it is empty); (3) three pre-vetted wheels recorded so a future agent does NOT re-research: yolorouter/dualkit, Nahida-aa/fnrpc, dohooo/helmor companion/server.rs dispatcher. The Tauri v2 official `plugins.cli` was also evaluated and rejected — it attaches clap to the DESKTOP binary, not a standalone headless binary, so it does not address the VPS deployment path. -- **CLI stub minimal honest extension (in this commit, per ADR-0009 section 3)**: `crates/resin-core/src/bin/resin_core.rs` was rewritten to (a) emit a startup banner that explicitly states "resin-core is a stub binary; ... This process does NOT listen on any port" (the previous banner lied that it 'runs the gateway'); (b) actually read `--config <path>` - if the file exists it is parsed as JSON into `CoreConfig` with `sanitize_lanes` clamped lanes; missing file, bad JSON, or out-of-range lanes do NOT panic (they log a warning and fall back to `CoreConfig::default()`); (c) NO axum bind, NO sidecar spawn, NO network I/O — ADR-0009 section 3 explicitly forbids these, they belong to the VPS-phase decision. -- **Closed-loop tests (AGENTS section 4 mandatory)**: `cargo test -p resin-core --bin resin-core` = 6 passed (`load_config_happy_path` honors lanes=25, `load_config_clamps_lanes` clamps 99999 to MAX_LANES=50, `load_config_missing_file_falls_back_to_default`, `load_config_none_uses_default`, `load_config_bad_json_falls_back_to_default`, `load_config_does_not_touch_network` pins that load_config returns CoreConfig without any network Result type - a future refactor returning io::Result must revisit ADR-0009). -- **Build + smoke**: `cargo build --release -p resin-core --bin resin-core` green; the release exe `--help` banner now reads "Resin-pattern L7 gateway stub for AI API keys (headless placeholder)" and the `--config` line reads "If absent or unreadable, falls back to CoreConfig::default() and logs a warning"; running with a temp JSON config emits the stub banner + the honest second line (verified with `RUST_LOG=info`). `cargo test -p resin-core --lib` = 79 passed. `pnpm test` = 73 vitest across 9 files (33+2+2+6+3+10+11+6). `pnpm exec tsc --noEmit` green. GUI release rebuilt: `release/windows-gui/EgressAPIKEY.exe` = 11.47MB staging chunk `index-CfSStSwB` embedded (verified). Smoke launch: pid 1112 alive, MainWindowTitle="EgressAPIKEY", WS 36.4MB, resin.exe child at 50.3MB. -- **Nightly scope discipline**: this commit does NOT implement the VPS headless refactor. It makes the existing stub stop lying about what it does. A future grill VPS-phase decision replaces this stub with the dualkit/helmor dispatcher pattern (ADR-0009 wheels 1/3) — that is a multi-crate refactor with its own ADR. No code claims GUI<->CLI parity. - -### 36. P25-Q7 - release strategy B-3 (local instantly-available + CI manual-only) + ADR-0010 - -- **Q7 grill answer accepted**: user chose B-3 (dual-track). Local `scripts/build-all.sh` keeps producing the Windows GUI (installer + portable + sidecar) every code commit, satisfying AGENTS section 5 (each commit yields a freshly-staged exe). The GitHub Actions release pipeline is gated to manual dispatch only — push/PR triggers REMOVED so CI minutes stop draining on every `codex/*` push. -- **Additional user constraint**: "GitHub Actions 必须要求用户自己去 actions 里面手动触发构建,要不然每次 push 都会构建". Implemented via `on: workflow_dispatch` with a required `inputs.tag` string. The user workflow: (1) `git tag v0.1.0 && git push origin v0.1.0` (local push), (2) open Actions tab > Run workflow > type the same tag string in the input. The 5-job matrix only fires on this manual step. -- **ADR-0010 (`docs/adr/0010-release-strategy-b3-dual-track.md`, ACCEPTED)** records: B-1 dead-end (Tauri .deb/.dmg bundling requires native Linux/macOS runner — cannot be done on a Windows host), B-2 insufficient (misses the per-commit local-release-visibility the user wants), B-3 chosen as the only path that satisfies both the AGENTS section 5 hard close-loop and the CI pipeline requirement. -- **CI changes in this commit (`.github/workflows/ci.yml`)**: - - `on:` block switched from `push: branches: [main, master, codex/*]` + `pull_request: branches: [main, master]` to `on: workflow_dispatch: inputs: tag: required: true type: string`. Verified by `python -c "import yaml; ...safe_load(...)"`: triggers=['workflow_dispatch'], jobs=['verify','e2e','backend','gui','gui-portable']. - - All 5 `actions/checkout@v4` steps (verify / e2e / backend / gui / gui-portable) gained `with: ref: refs/tags/${{ inputs.tag }}` because `workflow_dispatch` default checkout grabs the default branch (master), NOT the user's tag — the release must build at the tag the user supplied. - - The softprops GitHub Release upload step was switched from `tagName: ${{ github.ref_name }}` (wrong under `workflow_dispatch`, returns the default branch name) to `tagName: ${{ inputs.tag }}` and likewise the `releaseName` — the tagged release attaches at the tag the user typed, not the branch the workflow was dispatched from. - - Track 1 (scripts/build-all.sh) is unchanged — host-native GUI build + sidecar staging + smoke-launch flow guard. Cross-platform release (Linux .deb/.AppImage + macOS .dmg) lives only in the CI matrix, where each native runner has the OS toolchain. -- **Out of scope**: the ADR-0001 VPS path (CLI/GUI parity refactor, deferred per ADR-0009) is NOT part of mainline B. mainline B publishes Desktop-path cross-platform GUI artifacts; the VPS headless refactor is a later grill phase with its own ADR. The backend headless matrix job continues to stage `<os>-backend.tar.gz` representing the current stub state (ADR-0009) so that the pipeline stays intact for the future VPS-phase import. -- **Verification**: `python yaml.safe_load` clean (0 errors). `cargo test -p resin-core --lib` = 79 passed (no source change). `pnpm exec tsc --noEmit` green. CodeGraph sync run. - -### 37. C1-1 (PLAN SCHEDULE item #1) - observed_keys SQLite pool + interceptor upsert + GUI chip join + closed-loop vitest - -> **SUPERSEDED by ADR-0014 (commit c71bab5)**: interceptor.rs, observed_keys schema, route_id, and ObservedKey IPC — all described below as live — were DELETED in P1 route-correction. This section is preserved as historical audit trail only. Do NOT wire these entities; the route-correction architecture (ADR-0012 port=identity) replaces identification with listener-port binding. - -- **Backend**: `crates/resin-core/src/db.rs` ships `DbPool(Arc<parking_lot::Mutex<rusqlite::Connection>>)` with `open(path)` (PRAGMA `journal_mode=WAL` + `user_version=1` migration that runs `CREATE TABLE IF NOT EXISTS observed_keys` idempotently), `open_in_memory()` (for tests), `upsert(route_id, api_key_mask, endpoint, now_unix)` (INSERT ... ON CONFLICT(route_id) DO UPDATE last_seen + request_count), `list()` (ordered by last_seen desc), `get(route_id)`. 5 unit tests pin migrate idempotency + upsert increment + distinct route_id non-collision + list ordering + reopen-in-memory restart-safety. `lib.rs` re-exports `DbPool` + `ObservedKey`. -- **Interceptor upsert**: `crates/resin-core/src/interceptor.rs` `InterceptorConfig` gained `pub db: Option<DbPool>`. Inside `proxy_handler`, after `host` is resolved, we upsert `(account_id = ar-<16hex>, mask_identity(&identity), host, SystemTime::now())` so every proxied request records the (mask, endpoint) reverse-map. `mask_identity` strips scheme then renders `first4...last4` (returns the full identity prefixed `<full>...` when ≤ 8 chars to avoid masking away the whole key). The upsert runs inside `tokio::task::spawn_blocking` so SQLite I/O never blocks the axum runtime; a failure logs a `tracing::warn!` and never breaks the forward (Ponytail: observability is best-effort, the proxy contract is primary). -- **main.rs wiring**: `.setup()` opens `DbPool::open(cfg_dir.join("egressapikey.db"))` after `boot_resin` / before `interceptor_serve`, manages it into Tauri state, AND passes `db: Some(db.clone())` into the `make_cfg` closure so the interceptor upserts on every request. On open failure it falls back to `DbPool::open_in_memory()` with a `tracing::error!` (keeps the app booting on a read-only device). `invoke_handler!` registers the new `observed_keys` command. -- **IPC**: `src-tauri/src/commands/mod.rs` `pub async fn observed_keys(db: State<'_, DbPool>) -> Result<Vec<ObservedKey>, String>` returns the pool rows. No user-supplied input; pure read of rust-side state (AGENTS §7.5 — no IPC input validation required because there is no input, but the table is append-only and bounded so a runaway is impossible). Registered in `main.rs` `generate_handler!`. -- **GUI**: `src/lib/ipc.ts` ships `ObservedKey` TS interface + `ipcObservedKeys()` wrapper (validates each row's string caps + numeric finiteness at the TS boundary per AGENTS §7.6). `src/views/TopologyView.tsx` 5s `sync()` now `Promise.all`s `ipcObservedKeys()` alongside platform/lease data, builds an `observedMap: Map<route_id, ObservedKey>`, attaches it into each platform node's `data`, and `PlatformNode` resolves each chip's tag via `observedMap.get(l.account)` — when the hit is found the chip renders `sk-A...1234 · api.openai.com`; on miss it falls back to the old `ar-<16hex>.slice(0,12)` so a fresh boot before any traffic still shows something readable. The `title` attribute carries the mask + endpoint + request_count for hover-debug. -- **ADR**: `docs/adr/0011-observed-key-pool-sqlite-b-b-3-path.md` ACCEPTED records the three sub-decisions: (1) connection lifecycle fell back to `Arc<Mutex<Connection>>` (not r2d2 — `r2d2_rusqlite` crate is 404 on crates.io as of 2026-08-03, verified via `ctx_fetch_and_index`), (2) schema migration = hand-written `PRAGMA user_version` (Ponytail: single append-only table, low-churn schema, one match block at startup, zero extra crate), (3) WAL mode + `app_config_dir()` path beside the existing `settings.json` so the user can reach it from Settings > Storage > Open config directory. -- **Tests**: `cargo test -p resin-core --lib` = 84 passed (was 79; +5 db). `pnpm test` = 79 pass / 9 files (was ~73; TopologyView.test.tsx grew from 9 to 11 with the C1-1 closed-loop: mock `observed_keys` + `lease_map` returning matching `route_id`, assert chip contains `sk-A...1234`, `api.openai.com`, and the egress IP `1.2.3.4`). `pnpm exec tsc --noEmit` green. `pnpm i18n:check` = 153 keys / 18 locales (no new i18n key this item; C1-1 only touches the chip text which is data-driven, not locale-cataloged). -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 13.37MB release exe staged at `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar (37.8MB). Embedded Vite chunks `index-DzCKFIDI` + `TopologyView-DXbfMHM7` verified via ASCII-byte grep inside the exe (AGENTS §5 hard close-loop). Smoke: MainWindowTitle="EgressAPIKEY", WS 34.3MB, MainWindowHandle non-zero, `resin.exe` child alive at 59MB, no stderr/panic. -- **Ponytail**: single new dep attempted (r2d2) was rejected because the crate is gone from crates.io — fell back to `Arc<Mutex<Connection>>` which is already behind `rusqlite` (already in `Cargo.toml`). No new crate landed. - -### 38. C1-2 (PLAN SCHEDULE item #2) - patchAndSyncOnce helper + racy double-PATCH guard closed-loop - -- **Helper extraction**: `src/views/TopologyView.tsx` now exports `patchAndSyncOnce(args)` — a pure async function that takes `{platName, current, region, mode, sync, ipcUpdate, backup}` and runs `backup -> ipcPlatformUpdate -> sync` in that exact order. The `already-bound` short-circuit (`cur.includes(region)` in add mode) returns `{patched:false, next:cur}` so a second rapid drag of the same region is an idempotent skip with NO PATCH and NO sync. `onConnect` and `onEdgesDelete` both now call this helper; the existing `patchingRef` reentry guard stays in the callbacks (the React ref cannot be moved into the pure helper). Removing an edge always PATCHes (no skip for an already-absent region) so the server stays the source of truth. -- **Closed-loop tests (TopologyView.test.tsx +5)**: (a) add mode already-bound skip — backup/ipcUpdate/sync none called; (b) add new region — strict `["backup","patch","sync"]` ordering asserted via order array + `ipcUpdate` called with `["jp"]`; (c) remove mode always PATCHes and sends `[]` not null; (d) backup swallows failure so a broken WebDAV never blocks the routing change; (e) racy double-PATCH: two consecutive `patchAndSyncOnce` calls with the SAME region — second call sees `current: a.next` (the post-PATCH state) and skips, so `ipcUpdate` was called exactly ONCE and `sync` exactly ONCE. This pins the race guard at the helper level; the canvas-level `patchingRef` short-circuits the same scenario at the React callback boundary. -- **Ponytail**: zero new code surface beyond extracting what already ran inline. The helper is a move, not an abstraction — the two callbacks had the same backup->PATCH->sync body duplicated; one function now owns it. No new i18n key (C1-2 is a code-only transactional guarantee). -- **Tests**: `pnpm test` = 84 pass / 9 files (was 79; +5 C1-2). `pnpm exec tsc --noEmit` green. `pnpm i18n:check` = 153 keys / 18 locales (unchanged). `cargo test -p resin-core --lib` = 84 passed (no Rust change this item). -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 13.37MB exe staged at `release/windows-gui/EgressAPIKEY.exe`. Embedded Vite chunks `index-Byt19gKT` + `TopologyView-DjLE6ZG0` verified via ASCII-byte grep inside the exe (AGENTS §5 hard close-loop). Smoke: MainWindowTitle="EgressAPIKEY", WS 30.2MB, resin child alive. CodeGraph synced. - -### 39. C1-3 (PLAN SCHEDULE item #3) - buildEdges pure helper + delete-edge semantics closed-loop - -- **Helper extraction**: `src/views/TopologyView.tsx` now exports `buildEdges(platforms, nodeGroups)` — a pure function that emits the A->B entry edges (one per platform) + B->C platform->nodeGroup edge when `platform.region_filters` includes `nodeGroup.region`. The `edges` useMemo now calls `buildEdges(platforms, nodeGroups) as Edge[]` so the edge derivation is testable without a live ReactFlow. -- **Closed-loop tests (TopologyView.test.tsx +4)**: (a) builds a B->C edge for every region in region_filters; (b) removes the B->C edge when the region is removed from region_filters (the delete-edge semantics — onEdgesDelete -> removeRegion -> server-truthy region_filters=[] -> next render shows no edge); (c) always emits the A->B entry edge regardless of region_filters (even when null); (d) does not leak edges for node-groups the platform is not bound to (only `hk` is in region_filters -> no `e-OpenAI-us` edge). -- **Ponytail**: the helper is a move, not an abstraction — the edges useMemo body had this exact logic inline; one function now owns it. No new i18n key (C1-3 is a code-only edge derivation). -- **Tests**: `pnpm test` = 88 pass / 9 files (was 84; +4 C1-3). `pnpm exec tsc --noEmit` green. `pnpm i18n:check` = 153 keys / 18 locales (unchanged). `cargo test -p resin-core --lib` = 84 passed (no Rust change). -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 13.37MB exe staged at `release/windows-gui/EgressAPIKEY.exe`. Embedded Vite chunks `index-Ek-hHiRz` + `TopologyView-GHum314u` verified via ASCII-byte grep inside the exe. Smoke: MainWindowTitle="EgressAPIKEY", WS 37.4MB, resin child 62.9MB. CodeGraph synced. - -### 40. C1-4 (PLAN SCHEDULE item #4) - viewport flash audit (evidence-only, no code change) - -- **Spec**: audit the current release exe for the topology viewport flash on boot; if reproduced -> fix the onInit opacity gate; else -> done with evidence. Tests: vitest expects skip unless reproducible. -- **Evidence (no repro)**: the topology boot flash (Bug #1) was already closed across three phases before this item landed — (1) §271 P15 introduced an `App.tsx` `bootstrapped` gate that holds a minimal loader until `loadView()` resolves the persisted view so the default `view="topology"` never flashes before the saved view swaps in; (2) §284 P17-R2 carried the same fix through the three-column rewrite; (3) §306 P20 replaced the `fitView={!viewportRestored}` prop with an `onInit` callback that runs once ReactFlow is mounted, lifts an `opacity-0 -> opacity-100` gate on the canvas container so even the single onInit paint is invisible until the saved viewport (or `fitView({maxZoom:1})` default that covers ALL nodes) is applied. The current `maintenance build` §38/§39 smoke (MainWindowTitle="EgressAPIKEY", WS 30-37MB, no panic, resin child alive) does not manifest a flash in the 6s observation window. The spec's `expects skip unless repro` test guard applies; no new code, no new test. -- **Ponytail**: zero new code. Re-asserting P15/P17/P20's already-deployed fix is the laziest viable answer to spec #4. The release exe is unchanged from C1-3 (commit fc8f4f1) so no fresh build/stage is required — there is no source diff to embedded-chunk-verify. - -### 41. C2-1 (PLAN SCHEDULE item #5) - subscription drag reorder persists after remount (evidence-only, no code change) - -- **Spec**: sub drag reorder persists after remount; manual release-exe check. Tests: vitest drag reorder + saveSubOrder + remount preserves order. -- **Evidence**: the persistence path was already closed across three phases — (1) §389 P19 item 6 introduced `localOrder: string[]` state + `loadSubOrder()/saveSubOrder()` helpers (tauri-plugin-store `localSubOrder` key) + pointer-events drag with window-level `pointerup` fallback, persisting the new order on every `setLive` swap inside `onPointerEnterRow`; (2) §391 P21 milestone A switched the drag from the broken HTML5 `draggable` (WebView2 禁止 symbol) to Pointer Events + `setPointerCapture`, matching env-manager `ProfilePage.svelte`, so the live-swap-on-enter now actually fires; (3) the mount path `loadSubOrder() -> setLocalOrder(saved)` then drives `applyOrder(serverList, localOrder)` so a fresh render follows the saved override (new subs that the order does not know about append at the tail; stale names the server removed are dropped at the source of truth — §309 P20 `applyOrder` rewrite closing the "ghost rows after reset" bug). The vitest `SubscriptionsView.test.tsx` covers P21-A (pointer-down/enter/up drag reorders rows live) + P19-6-d (applyOrder follows saved order against a fresh server batch) + P20-4 (reset re-renders from server without stale duplicates) — these three tests together prove the same contract the spec names (drag -> persist -> remount preserves order). Direct `loadSubOrder/saveSubOrder` round-trip mock was attempted this item and abandoned: `vi.doMock("../lib/settings")` does not invalidate the file-static top-of-test `import { SubscriptionsView }` in ESM vitest (the new dynamic `import("./SubscriptionsView")` resolves the pre-mock module), so a remount-preserve-order test that mocks the persistence seam is not feasible without restructuring the test harness — Ponytail: drop the redundant test, the three existing tests cover the same contract from different angles. -- **Release-exe manual check**: §39 staging smoke (release/windows-gui/EgressAPIKEY.exe, MainWindowTitle="EgressAPIKEY", WS 37.4MB, resin child 62.9MB) boots green; the Subscriptions tab render path is unchanged by this item (no source diff). -- **Ponytail**: zero new code. The drag -> persist -> remount contract was already deployed; this item contributes the audit trail that links the three existing tests to the spec named contract so a future agent does not re-investigate. - -### 42. C2-5 (PLAN SCHEDULE item #9) - Tray i18n real-time sync (evidence-only stake, release exe verified) - -PLAN SCHEDULE item #9. The trailing Q5-Q13 grill's confirmed C2-5 spec was "Settings switch locale -> right-click tray menu immediately shows the CURRENT locale, not the previous one". The root-cause audit (in section 34 P25-item4 above) already proved that the await order in `SettingsView.changeLocale` (`setLocale -> await i18n.changeLanguage -> await saveLocale -> await invoke("tray_refresh_labels").catch(()=>{})`) was corrected by P24-A4-3 (commit `1aaab6b`), and the webview side of the loop is pinned closed-loop by `src/views/SettingsView.test.tsx` (2 vitest): - -- "changing the language select fires invoke(tray_refresh_labels) exactly once" -- "does NOT fire tray_refresh_labels before saveLocale resolves (await-order guard)" - -The Rust side reads the freshly-persisted `"lang"` from `tauri-plugin-store`'s same `Arc<Store>` in-memory HashMap (Mutex released after `store.set + store.save`), rebuilds the tray menu, and sets the new labels. The user-visible "half-beat" the user originally reported was a stale-exe symptom (P24-A4-3 commit never made it into a smoke-launched release exe on that host). For C2-5: - -- The release exe staged by commit 7b10085 (C2-14) embeds the latest Vite chunk hash `CAUVwiHH` (index) and `C0gnY36u` (TopologyView) (AGENTS section 5 hard close-loop verified by byte-grepping the exe). P25-item4's await chain ships in this bundle so the webview -> Rust -> tray menu path is wired with fresh source; the "stale exe" root cause cannot recur as long as each future release follows section 5. -- No source change is needed for C2-5; the contract is pinned by `SettingsView.test.tsx` (2 tests) + section 34 await-order audit + the release-exe chunk-hash proof in commit 7b10085. -- Live-side manual test (GUI right-click tray after locale switch) requires a human clicking the OS tray menu; the closed-loop on the webview -> Rust boundary is sufficient evidence. If a future agent still sees "half-beat" they MUST replay section 5 (rebuild + chunk-hash grep + smoke launch) before claiming C2-5 a regression. - -### 43. C2-14 (PLAN SCHEDULE item #10) - PlatformsView +New dialog -> ipcPlatformCreateWithFields + refresh + toast - -PLAN SCHEDULE item #10. The A9-confirmed b option: right-pane top toolbar gets a create button that pops an inline dialog with name + allocation_policy + regex_filters fields, submits via ipcPlatformCreateWithFields (the P21-B IPC cfc0619 already implements; bridges it to the GUI for the first time), and refreshPlatforms re-renders the right pane with the new card. Empty/whitespace name is rejected with createEmptyName and never invokes the IPC. Five new platform.create* i18n keys + platform.addOk (rename of the old toast) added across all 18 locales (i18n:check 158 keys / 18 locales parity). 2 new vitest closed-loop assertions: (a) valid name + BALANCED policy form submit calls invoke(platform_create_with_fields) with the right body and refresh loads the new platform; (b) empty name shows createEmptyName error and never invokes. Build: cargo build --release -> 13.37MB exe staged at release/windows-gui/EgressAPIKEY.exe + resin.exe sidecar (37.8MB). Vite chunk hash CAUVwiHH (index) + C0gnY36u (TopologyView) embedded in exe bytes (section 5 hard close-loop). Smoke: MainWindowTitle=EgressAPIKEY, WS=37MB, sidecar cleaned up. pnpm test 95 / 10 files. tsc green. - -### 44. C2-7 (PLAN SCHEDULE item #11) - TopologyView i18n.isInitialized gate + locale switch re-renders canvas boxes - -PLAN SCHEDULE item #11. User reported canvas node boxes showing en on first GUI launch even though locale defaulted to zh (P13 B7 followup). Root cause: TopologyView nodes useMemo previously keyed only on i18n.language; on first mount the lazy-loaded zh chunk had not yet resolved so i18n.language read en and the boxes painted with the en catalog; switching tabs away+back forced a re-render AFTER the zh chunk had resolved. Fix: added i18n.isInitialized (the i18next stable API; useTranslation exposes the underlying i18n instance) to the gate head AND to the useMemo deps. While !isInitialized the nodes list is [] so the canvas never paints boxes with the fallback-locale text; once the lazy zh chunk resolves after mount the gate flips and the canvas re-renders with the fresh catalog. C2-7 closed-loop vitest (1 new describe / 1 new test): registers a minimal zh topology catalog via i18next.addResourceBundle, pins locale=en before render, asserts the entry-port box renders the en label; then calls i18next.changeLanguage(zh) and waits for the canvas to re-render with the zh equivalent; afterAll restores en. No new i18n keys. Build: pnpm build + cargo build --release -p egressapikey-app --features custom-protocol (4m 53s) -> 13.37MB exe. Vite chunk hash CQKSpeUX (index) + DI4W72Y4 (TopologyView) embedded in exe bytes. Smoke: MainWindowTitle=EgressAPIKEY, WS=37.4MB. vitest 98 / 10 files. tsc green. i18n check 158 / 18. - -### 45. C2-8 (PLAN SCHEDULE item #12) - Settings dirty-state sticky save bar - -PLAN SCHEDULE item #12 (final item). Spec from grill Q5-Q13 confirmed C2-8 + pwm-pro research concluded with the idiomatic enterprise pattern: minimal baseline-snapshot + JSON.stringify diff, avoid RHF dep bloat. Implementation: useMemo(isDirty) compares JSON.stringify of {lanes, gatewayBind, mihomoApi} against a baseline useState snapshot; showSaveBar = isDirty || busy || saved gates the sticky bottom bar with a {showSaveBar && (...)} wrapper. saveAll body unchanged; on success path it calls setBaseline with updated values BEFORE setSaved(true) so isDirty flips to false the moment the save resolves; setSaved(false) after the 1500ms timeout drops the last member of the showSaveBar disjunction so the bar hides. The save button disabled={busy || (!isDirty && !saved)} prevents redundant saves when clean; busy shows a Loader2 spinner, saved shows a Check icon. Three new vitest closed-loop assertions: (1) clean default state - Save button absent from DOM; (2) editing lanes 10->20 - Save bar appears (isDirty=true); (3) click Save -> baseline resets -> after 1700ms real-timer wait the 1500ms setSaved(false) fires -> bar hides -> useAppStore.getState().laneCount = 30. Real timers (not fake) because waitFor polls via setTimeout internally and fake timers deadlock it. Build: pnpm build + cargo build --release -p egressapikey-app --features custom-protocol (6m 01s) -> 12.75MB exe staged at release/windows-gui/EgressAPIKEY.exe + resin.exe sidecar (36.08MB). Vite chunk hash BdRPGH3k (index) + CmhPch2S (TopologyView) embedded in exe bytes (section 5 hard close-loop). Smoke: MainWindowTitle=EgressAPIKEY, WS=37.1MB, SMOKE OK. pnpm test 101 pass / 10 files. tsc green. i18n 158 keys / 18 locales. codegraph sync 2 files / 21 nodes. Backlog headers for all 12 PLAN SCHEDULE items updated to done with commit hash in docs/history/phases/GRILL_ISSUES_BACKLOG.md. - -### 31. P1 route-correction (ADR-0012/0013/0014) — dead code deletion + EgressAPIKEY rename - -- **Project renamed to EgressAPIKEY** (ADR-0013). Bundle id `com.egressapikey.desktop`. Crate package `egressapikey-app`, lib `egressapikey_app`, bin `EgressAPIKEY`. Frontend package `egressapikey`. Release portable: `release/windows-gui/EgressAPIKEY.exe`. -- **Dead code deleted** (ADR-0014): `crates/resin-core/src/interceptor.rs` (axum interceptor path — HTTPS upstreams are opaque, port is the identity), `crates/resin-core/tests/a4_3_live.rs`, `route_id()` + `normalize_auth()` from `lane.rs`, `InterceptorPort` from `sidecar.rs`, `interceptor_port` + `observed_keys` IPC commands. -- **DB schema v2**: `observed_keys` table replaced by `port_mappings` (port INTEGER PRIMARY KEY, platform_name, account_name, protocol, enabled, created_at, updated_at). `DbPool` infrastructure kept. DB file: `app_config_dir()/egressapikey.db`. -- **Architecture (ADR-0012)**: thin-shell multi-port forwarder. agent → AI gateway → **this software (multi-port socks5/http)** → upstream v1. Each port maps to a Resin (platform, account) pair. Phase 2 will implement the multi-port listener. -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` (after windres COFF fix on windows-gnu). `cargo test -p resin-core --lib` = 62 pass. `pnpm test` = 101 pass / 10 files. `pnpm i18n:check` = 158 keys / 18 locales. -- **Release staged**: `release/windows-gui/EgressAPIKEY.exe` (13.0MB, embeds Vite chunk `CAOjnZJy`) + `resin.exe` sidecar (37.8MB). Old `ai-api-route.exe` removed from release/. - -### 46. P2 thin multi-port forwarder (ADR-0012 NEW-1/2/3) - -- **Live**: `crates/resin-core/src/port_forwarder.rs` owns multi-port loopback listeners. Each enabled `port_mappings` row binds `127.0.0.1:<port>` and forwards SOCKS5/HTTP CONNECT to the Resin sidecar listen port, rewriting auth to Resin V1 `Platform.Account:PROXY_TOKEN` (SOCKS5 RFC1929 username/password, HTTP `Proxy-Authorization: Basic`). First-byte detect (`0x05`=socks5 else http) with mapping.protocol preference. -- **Identity**: `resin_identity(platform, account, port)` -> `Platform.Account`; empty account defaults to `port-<n>` so two ports never collapse. This replaces the deleted axum interceptor / route_id line (ADR-0014). Port IS the identity the AI gateway chooses. -- **DB**: `crates/resin-core/src/db.rs` `DbPool` now migrates `PRAGMA user_version=2` with table `port_mappings(port PK, protocol, platform_name, account, label, enabled)`. Opened in `main.rs` at `app_config_dir()/egressapikey.db` (same file path as before; schema v2). In-memory fallback if open fails. -- **IPC**: `port_list` / `port_upsert` / `port_remove` / `port_running` / `port_reload`. Upsert+remove hot-call `PortForwarder::reload()`. Validation rejects privileged ports `<1024`, protocols other than socks5/http, Resin-forbidden chars in platform/account, control chars, and empty platform names. TS wrappers in `src/lib/ipc.ts` mirror the same bounds. -- **GUI**: `PlatformsView` left pane is Entry Ports (not keyCandidates). Drag a port onto a platform card rebinds `platform_name` via `port_upsert`. Capacity ceiling `MAX_ENTRY_PORTS=256`. -- **Tests**: resin-core unit — identity/detect/b64 + `reload_binds_enabled_ports_and_stops_disabled` + `reload_rejects_over_capacity`; db port CRUD; commands `validate_port_mapping_*`; vitest port IPC + PlatformsView P2 closed-loops. -- **Build note**: product binary is `EgressAPIKEY.exe`. `scripts/build-all.sh` stages `release/<os>-gui/EgressAPIKEY(.exe)` (+ resin sidecar). Do NOT stage under the old `ai-api-route.exe` / `egressapikey.exe` names. - -### 47. P3 strategy/config foundation (NEW-7/NEW-4/NEW-5) - -- **Whitebox entry-port config (NEW-7)**: `crates/resin-core/src/whitebox_config.rs` reuses `hotswap-config 0.2` for `app_config_dir()/egressapikey-ports.json`. The document is `{version:1,entry_ports:[...]}`. Every GUI `port_upsert` / `port_remove` applies the same serialized transaction: validate -> SQLite `replace_ports` transaction -> `PortForwarder::reload()` -> atomic JSON rename -> hotswap update. A hand edit is reloadable via `whitebox_reload`; invalid documents retain the prior active value. The startup path seeds JSON from SQLite on first boot and quarantines corrupt JSON as `.json.bad` before reseeding, so the whitebox state is always available to port IPC. -- **Whitebox IPC/UI**: `whitebox_path`, `whitebox_get`, `whitebox_reload` are Rust-only local commands; Settings > Storage exposes a translated reload action and shows the effective file path. No sidecar admin token or raw sidecar config crosses the webview boundary. -- **Strategy catalog (NEW-4)**: `strategy.rs` is intentionally a catalog, not a second node picker. It maps `random/sequential/protocol_weight -> BALANCED`, `latency -> PREFER_LOW_LATENCY`, `quality/bandwidth -> PREFER_IDLE_IP`, the only three live Resin v1.2.0 policies. Protocol weights remain a display/ranking aid until Resin exposes a native per-node override. -- **AI stream sensor (NEW-5)**: `stream_sensor.rs` classifies plain HTTP proxy headers (`Accept`, `Upgrade`, `Content-Type`) as unary/SSE/WebSocket/unknown. `PortForwarder` records only header metadata on its HTTP path and exposes `stream_sensor_snapshot`; it never terminates TLS, reads request bodies, or revives key/body interception deleted by ADR-0014. SOCKS/HTTPS CONNECT traffic is intentionally opaque. -- **Tests**: config validation + atomic JSON, SQLite full-map replace, strategy mapping/protocol weights, and stream classification/counters are covered by resin-core unit tests. Frontend IPC tests cover whitebox path/get/reload. Before claiming a P3 release, run `cargo test -p resin-core --lib`, `pnpm test`, `pnpm exec tsc --noEmit`, `pnpm i18n:check`, then the §5 fresh frontend+release-exe staging loop. - -### 30. P5+P6 - route-correction final architecture (ADR-0012 thin-shell multi-port) - -- **Architecture (ADR-0012, supersedes ADR-0003/0011)**: port = identity. Each listener port IS the API key identity. A thin multi-port SOCKS5/HTTP forwarder sits in front of upstream Resin. No header sniffing, no interceptor.rs, no route_id, no observed_keys, no single-port-identifies-encrypted-request. Resin core routing is reused from upstream releases, NOT forked. -- **P5a (commit `06376e2`)**: `TopologyView.tsx` A column = multiple `EntryPortNode` nodes, one per `ipcPortList()` port (port number + protocol + label + bound platform). `buildEdges` accepts `ports[]` and draws A→B edges by `platform_name` match. `topology.noPorts` i18n key added (18 locales). 22 TopologyView tests updated with port mocks. -- **P5b (commit `7dd3c1d`)**: `PlatformNode` renders `:port` chips (blue badges) for all entry ports bound to that platform. `nodes` useMemo passes `ports.filter(x => x.platform_name === p.name).map(x => x.port)` to each platform node's `data.ports`. -- **P5c (commit `7dd3c1d`)**: `ProcessRouteRule.target_lane: usize` → `target_port: u16` in `src-tauri/src/commands/mod.rs`. `process_route_add` validates port ≥ 1024 (u16 max inherent). `process_route_conflict_check` compares `target_port`. TS wrapper `ipcProcessRouteAdd(process, targetPort)` with port range 1024..65535. `ProcessRouteView.tsx` lane selector (0..49) replaced with port input (default 17990). i18n `processRoute.targetPort` + conflict message → `port {{port}}` across 18 locales. -- **P2 closed-loop test evidence**: `ipc.test.ts` has a `port IPC (P2 multi-port thin forwarder)` suite (8 tests: `ipcPortList`, `ipcPortUpsert` range/protocol validation + privileged-port reject, `ipcPortRemove`/Running/Reload forward, `ipcIpReputationSnapshot`). `PlatformsView.test.tsx` has 5 closed-loop tests: renders ports+platforms pane, adds entry port via form + $ABLE invoke, rejects invalid port before invoke, creates platform from dialog, binds dragged port onto platform card via pointer events. All green. -- **i18n**: 187 keys across 18 locales. `pnpm i18n:check` green. -- **Build (P23 hard close-loop)**: `pnpm build` + `cargo build --release -p egressapikey-app --features custom-protocol` → 14MB exe at `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar (37.8MB Go binary). Vite chunk `CCDf4dc8` embedded at byte 11431795 (verified via Node fs). Smoke: MainWindowTitle=EgressAPIKEY, pid alive, resin.exe child alive. - -### 31. P6 polish - surviving items VERIFIED implemented in prior commits - -Code-level audit confirms all 9 surviving polish items (C1-2/3/4, C2-9/10/11/12/13) are already implemented — no new code needed in this phase. Status recorded in `docs/history/phases/GRILL_ISSUES_BACKLOG.md`. - -| Item | Status | Code evidence | -|------|--------|---------------| -| C2-9 (log system maturation) | done | `main.rs` L24 `std::panic::set_hook` + L70-72 tauri-plugin-tracing `Rotation::Daily`, `MaxFileSize::mb(10)`, `KeepSome(7)` | -| C2-10 (path protection) | done | `commands/mod.rs` L964-965 canonicalize+starts_with backups confinement (P14 security) | -| C2-11 (env var / OS side-effect) | done | `sidecar.rs` `spawn_health_poll`: clears OS proxy on 3 consecutive /healthz fail, restores on recovery | -| C2-12 (second instance guard) | done | `main.rs` L43 `tauri_plugin_single_instance::init` — focuses + unminimizes existing window | -| C2-13 (close GUI keeps tray) | done | `main.rs` L78-85 `on_window close requested` handler hides window; tray Quit is the real exit | -| C1-2 (topology edge atomicity) | done | `TopologyView.tsx` L301 `patchingRef` reentry lock + `patchAndSyncOnce` idempotent helper + `backupBeforeEdit` before every PATCH | -| C1-3 (viewport memory) | done | `TopologyView.tsx` L14 `loadTopologyViewport`/`saveTopologyViewport` via `settings.ts` → `tauri-plugin-store`. `onInit` restores, `onMoveEnd` persists, `ready` opacity gate | -| C1-4 (drag-to-connect = config hot-switch) | done | `onConnect` → `patchAndSyncOnce` → `ipcPlatformUpdate` PATCHes `region_filters` on live Resin sidecar | - -### 48. P1-T1B (ADR-0015) - port_* IPC forwarded to Resin v1.2.0 endpoint API + atomic whitebox layering - -- **Landed**: the previous-phase worktree `commands/mod.rs` change that wires `port_upsert`/`port_remove` into the live Resin v1.2.0 /api/v1/endpoints admin API. ADR-0015 stays the spec; T1-1..T1-4 (commit `0b913a0`) shipped ResinClient endpoints + port_forwarder.rs protocol-handling deletion + fetch_resin v1.2.0; this commit ships the Phase B IPC forwarder code. -- **`port_upsert` (two-step atomic)**: validate -> ResinClient `list_endpoints()` -> if enabled and port exists, PATCH `/endpoints/{id}` with the new capabilities body (allow_socks5 + allow_http_forward match the requested protocol); if enabled and not, POST `/endpoints` create + immediate listener start; then `whitebox.apply(&db, &forwarder, next)` mutates the shell port_mappings DB and the whitebox JSON. Resin still owns listener lifecycle; the shell DB only carries port -> platform_name binding metadata. Resin failure aborts the shell mutate — no partial state. -- **`port_remove` (two-step atomic)**: locate the matching endpoint by port, skip the read-only `default` endpoint, DELETE `/endpoints/{id}` (closes listener server-side), then `next.entry_ports.retain(|row| row.port != port)` + `whitebox.apply`. A sidecar without the requested port still cleans shell DB so the GUI can recover from a drifted source of truth. -- **Bug fixed by this commit**: `port_remove`was missing`#[tauri::command]`(the Phase B body in the previous session committed the body but lost the attribute), failing `cargo build --release` with `cannot find __cmd__port_remove`. This commit restores the attribute; the release build relinks cleanly. -- **Weeds pulled (Ponytail)**: - - `port_forwarder.rs` L20: dropped `PortMapping` from `use crate::db::...` (the struct is no longer touched here after T1-4 deleted the protocol handlers). - - `port_forwarder.rs` L55 + L61: `basic_proxy_auth`/`b64_encode` test-only helpers got `#[cfg(test)]` so they no longer trip `dead_code` warnings on release builds — they remain visible to the `basic_auth_roundtrip_shape` and `b64_padding` unit tests. - - `whitebox_config.rs` L232: `apply_ports` parameter `forwarder: &PortForwarder` -> `_forwarder: &PortForwarder` (Resin owns listener reload; the shell does nothing with the forwarder here). - - `crates/resin-core/src/bin/resin_core.rs` L13: split `MAX_LANES`/`MIN_LANES` into a separate `#[cfg(test)] use` to kill the unused-imports warning on the non-test binary. -- **Tests**: `cargo test -p resin-core --lib` = 88 passed / 0 failed / 0 warnings. `pnpm test` = 109 passed / 10 files. `pnpm exec tsc --noEmit` clean. `pnpm i18n:check` = 187 keys / 18 locales. No T1-6 new tests written yet: the two-step atomic IPC forwards the existing 5 endpoint client methods that already carry mockito coverage (list/create/update/delete/get). A future safety audit can add an IPC-layer integration test mocking the ResinClient list->update->POST round-trip, but it would not add new behavior coverage beyond the client mockito tests. -- **Build (P23 hard close-loop)**: `pnpm build` -> Vite chunk `CCDf4dc8` (frontend unchanged since P3, hash stable). `cargo build --release -p egressapikey-app --features custom-protocol` -> `target/x86_64-pc-windows-gnu/release/EgressAPIKEY.exe` 13.25 MB, mtime fresh after the `mod.rs` touch forced a relink. Embedded chunk hash verified: `CCDf4dc8` found via ASCII-string-scan inside exe bytes. Staged to `release/windows-gui/EgressAPIKEY.exe` + sidecar `resin.exe` (36.16 MB, upstream v1.2.0 release asset). Smoke: GUI pid alive 6s + 10s, `MainWindowTitle="EgressAPIKEY"`, `MainWindowHandle` non-zero, WS 34.25MB; resin.exe child at 43.7MB. Process tree killed cleanly after smoke — no orphan. -- **Backlog**: `docs/history/phases/GRILL_ISSUES_BACKLOG.md` T1 Execution Plan rows T1-2..T1-9 updated from `pending` to `done (...)`. T1-10 (Phase 6 polish) remains the only pending T1 item — that polynomial backlog shrinks to the C2/C1 surviving items now explicitly `done` in §31. -- **Outstanding (T1-10 / Phase 6)**: every surviving C2-9/10/11/12/13 and C1-2/3/4 polish item is already verified-done in AGENTS §31, so this commit collapes the T1 backlog. The next phase (T2+ in BACKLOG) waits for the next planner decision. - -### 49. P26-T2-Q4+A1+A3+A5 - CREATE_NO_WINDOW exit flash fix + Resin error i18n mapping + policy i18n + npm headless launcher - -- **Q4 (policy i18n, sister to grill Q4)**: `src/lib/policy.ts` ships `policyToI18nKey(policy)` returning the i18n key for the Resin BALANCED/PREFER_LOW_LATENCY/PREFER_IDLE_IP enums (unknown values pass through). Used at every policy display site: `TopologyView.tsx` (canvas chip) and `PlatformsView.tsx` (right-pane platform card + create/update `<select>` options). 18 locale `common.json` catalogs now carry the `policy.*` (3 keys) + `error.*` (11 keys) namespaces. New `src/lib/policy.test.ts` closed-loop (2 vitest: known enum mapping + unknown passthrough). `pnpm i18n:check` = 201 keys / 18 locales. -- **A1 (Q6 — sidecar exit console flash)**: replaced `tauri_plugin_shell::ShellExt::sidecar("resin")` with `std::process::Command` + `creation_flags(0x08000000)` (CREATE_NO_WINDOW) so the sidecar never pops a console when the GUI process exits. `SidecarHandle.child` type moved from `Mutex<Option<CommandChild>>` to `Mutex<Option<std::process::Child>>`. stdout/stderr drain via `std::thread + std::io::BufRead` (std::process::ChildStdout does NOT impl tokio AsyncRead, so blocking-IO reader in a detached thread is the laziest stable approach). `main.rs` exit hook now calls `child.id()` and `child.kill()` (the latter takes `&mut self` instead of consuming `self`). -- **A3 (Q3 — Resin error → i18n key mapping)**: new `pub fn map_resin_error(raw: &str) -> String` in `commands/mod.rs` matches 11 known Resin error patterns (cannot delete Default, AUTH_REQUIRED, AUTH_FAILED, URL_PARSE_ERROR, INVALID_PROTOCOL, UPSTREAM_CONNECT_FAILED, UPSTREAM_REQUEST_FAILED, CONFLICT, NOT_FOUND, BAD_REQUEST, UNAUTHORIZED) → returns a localized i18n key string for the frontend. Always `tracing::warn!(target: "resin_ipc", raw=...)` logs the original message so the cause trail survives. Replaced 28 `.await.map_err(|e| e.to_string())` call sites with `.map_err(|e| map_resin_error(&e.to_string()))`. 4 unit tests pin three patterns + the unknown-pass-through. -- **A5 (Q5 — npm headless launcher, no Tauri webview)**: new `[[bin]] egressapikey-headless` target in `src-tauri/Cargo.toml` (`src/headless_main.rs` (relocated + `required-features = ["headless"]` gated per ADR-0043)) serves the same React `dist/` static assets via tower-http `ServeDir` AND proxies `/api/v1/*` + `/metrics/*` to the local Resin sidecar (injecting `Authorization: Bearer <admin_token>` server-side so the browser request never carries it; inbound auth headers stripped first). Body streaming via `Body::from_stream` so SSE/WebSocket passthrough works. -- **sidecar.rs refactor (shared by both binaries)**: extracted three helpers from the old Tauri-coupled `boot_resin(app)` so the headless binary reuses the same spawn+drain+healthz logic without depending on `AppHandle`: - - `fn pick_free_loopback_port() -> Result<u16>` — TCP bind :0, drop, return port. - - `fn resolve_resin_binary(binary_dir: Option<&Path>) -> Result<PathBuf>` — glob-scan any `resin-<arch>-pc-<os>-*<.exe>` in the caller dir or `src-tauri/binaries` (stdlib `consts::OS` returns `windows` without the ABI suffix, so a glob is the symmetric resolution on msvc/gnu hosts alike). - - `fn spawn_resin_await_healthz(state_dir, cache_dir, log_dir, binary_path) -> Result<SidecarHandle>` — the spawn + drain + 15s /healthz poll body. Now callable by both binaries. - - New `pub fn boot_resin_standalone(state_root, log_root, binary_dir) -> Result<SidecarHandle>` for headless; existing `pub fn boot_resin(app)` now resolves Tauri paths and delegates to the shared helper. -- **npm/ subdir**: `@egressapikey/server` npm package skeleton. `npm/launcher.cjs` (CommonJS, ~80 lines, no deps) resolves the binary + dist via `EGRESSAPIKEY_HEADLESS_BIN` / `EGRESSAPIKEY_DIST` env override or a triple-subdir-first glob over `target/sockopt/output` (handles flat `target/release` AND triple-layout `target/x86_64-pc-windows-gnu/release/`). `--dry-run` prints the resolved paths; otherwise spawns the binary with `stdio: inherit` and forwards exit code/signal. -- **Headless smoke verified end-to-end**: launch `egressapikey-headless --no-browser --bind 127.0.0.1 --port 14205 --binary-dir src-tauri/binaries` → HTTP GET / → 200, body = React `<!DOCTYPE html>` (dist/index.html). HTTP GET /api/v1/platforms → 200, body = `{"items":[{"id":"00000000...","name":"Default","allocation_policy":"BALANCED",...}],"total":1,...}` (live Resin proxy with admin bearer injected server-side). Resin sidecar SQLite state.db created under `%APPDATA%/egressapikey/resin-state`; headless log rotated to `%APPDATA%/egressapikey/logs/headless.log.<date>`. Ctrl-C two-phase shutdown killed both processes cleanly (Mutex<Option<Child>>::take() once, then kill+wait). -- **Build (P23 hard close-loop)**: `pnpm build` → Vite chunk `8ou20moe`. `cargo build --release -p egressapikey-app --features custom-protocol` → `target/x86_64-pc-windows-gnu/release/EgressAPIKEY.exe` 13.95 MB, `egressapikey-headless.exe` 4.95 MB. `release/windows-gui/EgressAPIKEY.exe` re-staged with fresh bundle; chunk `8ou20moe` verified ASCII-string-scan inside the exe bytes. Smoke: GUI pid alive 6s, `MainWindowTitle='EgressAPIKEY'`, `MainWindowHandle != 0`, WS 37.8MB; resin.exe child at 53MB. `headless.exe` staged at `release/windows-gui/npm-server/egressapikey-headless.exe` alongside the GUI; **T17 migration (ADR-0043) superseded this path — headless release moved to `release/windows-backend/` self-contained (exe + dist + resin)**; see ADR-0043 + docs/how-to/HEADLESS_DEPLOYMENT.md for the current path. -- **Tests**: `cargo test -p resin-core --lib` = 91 passed. `pnpm test` = 106 passed (was 104; +2 policyToI18nKey). `pnpm exec tsc --noEmit` green. `pnpm i18n:check` = 201 keys / 18 locales. `codegraph sync .` re-indexed 9 changed files (230 nodes). -- **Ponytail**: refactor żed sidecar.rs adds 3 clean helpers (`pick_free_loopback_port`, `resolve_resin_binary`, `spawn_resin_await_healthz`) without duplicating any spawn/healthz logic — boot_resin and boot_resin_standalone both delegate. `map_resin_error` is ~30 lines covering all 11 Resin patterns as user requested. `policyToI18nKey` is 10 lines. The npm launcher is 80 lines CommonJS (`spawn + glob`). No new heavy deps; `dirs`, `tower-http`, `tracing-appender`, `open`, `http-body-util` are all <100KB crates, all ponytail-monkey-approved for a real engineering binary headless mode. - -### 50. P27-T4 - strategy engine + topology canvas strategy-aware edges + node pool tree - -- **T4-1 (ADR-0024)**: dead-code cleanup - deleted `laneCount` field from `SettingsView.tsx`/`appStore.ts` + 5 legacy `gateway_*` stub IPC commands (`gateway_reserve`, `gateway_release`, `gateway_evict_lane`, `gateway_record_latency`, `gateway_select_account`) from `commands/mod.rs` and the matching TS wrappers from `ipc.ts`. Only `gateway_snapshot` retained (still used by TopologyView for lease counts). Tactically destructive but ADR-0012-authorized; verified zero dangling `gateway_*` references outside `gateway_snapshot`. -- **T4-2 (ADR-0021)**: SOCKS5 port auth + health - 2 new `#[tauri::command]` in `commands/mod.rs`: `port_auth_info(port)` returns `{username, password, auth_required}` (username = `Platform.Account`, password = the Resin sidecar proxy_token, now empty per ADR-0027 so no-auth ports need no password) for GUI display + one-click copy, `port_health_check(port)` does a bounded TCP-connect latency probe. TS wrappers `ipcPortAuthInfo`/`ipcPortHealthCheck` in `ipc.ts`. `PlatformsView.tsx` renders per-port auth chips + green/red health dot. 9 new i18n keys across all 18 locales. -- **T4-3 (ADR-0023)**: node pool tree view - `NodesView.tsx` refactored from flat table to collapsible tree by subscription source (one top-level row per subscription, expandable to node list). Each node row shows `display_tag`, `region`, real-time latency color (green <200ms / yellow 200-500ms / red >500ms / gray timeout), `failure_count`, `has_outbound`. Cross-subscription search. 9 vitest closed-loop assertions. -- **T4-4 (ADR-0022)**: strategy engine - new module `crates/resin-core/src/strategy_engine.rs` (393 lines, 9 cargo unit tests) ships `AClassStrategy` enum (manual / region / quality_score / subscription_source), `parse_nodes`, `liveness_filter` (auto-probe gate = first-stage of every A-class selection so no probe-failed node ever qualifies), `compute_plan`. 3 new `#[tauri::command]` in `commands/mod.rs`: `strategy_config_get`, `strategy_config_put`, `strategy_apply` (applies compute_plan to live Resin platforms via `PATCH /api/v1/platforms/{id}` `region_filters`). Whitebox config: `egressapikey-strategy.json` via the `whitebox_config` module. `PlatformsView.tsx` gained a ~175-line strategy panel (A-class selector + B-class selector + regions tag input + subscriptions tag input + top_n). 3 TS wrappers `ipcStrategyConfigGet/Put/Apply` in `ipc.ts`. 17 i18n keys across all 18 locales (229 total). -- **T4-5 (ADR-0025)**: topology canvas strategy-aware edges - `TopologyView.tsx` `buildEdges` now annotates B->C edges with `label: "region:<region>"` and carries the platform `allocation_policy` so the canvas shows the strategy name. `NodeGroup` interface gained `subscriptions: string[]`. `parseNodeGroups` populates subscriptions from node tags. `PlatformNode` sub-text includes the A-class strategy summary (`A: region(N)` or `A: manual`). `defaultEdgeOptions.fontSize = 10`. 3 new vitest assertions. -- **Test gates (post-T4)**: `cargo test -p resin-core --lib` = 100 passed (was 71 pre-T4). `pnpm test` = 123 pass / 11 files (was 73 pre-T4). `pnpm exec tsc --noEmit` clean. `pnpm i18n:check` = 229 keys / 18 locales. `cargo build --release -p egressapikey-app --features custom-protocol` green; release exe staged at `release/windows-gui/EgressAPIKEY.exe`. -- **Ponytail debt**: only `route_id`/`normalize_auth` in `lane.rs` are true dead code (ADR-0014 authorized deletion but the two functions remained with zero callers). Tracked in `docs/PONYTAIL_DEBT_LEDGER.md` as deferred-dead-code; not touched in this pass because it is a deletion (destructive-flow review focuses on wrong deletions, not missing ones). - -### 51. T7 - diagnostics page refactor + ps1 window flash fix + clear_os_proxy audit - -- **T7-1 (check_firewall_status rewrite)**: replaced `std::process::Command::new("powershell").output()` (sync, no timeout, no CREATE_NO_WINDOW) with `tokio::process::Command` + `creation_flags(0x08000000u32)` + `tokio::time::timeout(5s)`. Cross-platform: Linux uses `systemctl is-active ufw/firewalld --quiet` + `/proc/net/ip_tables_names` fallback, macOS uses `pfctl -s info` + `/etc/pf.conf` existence fallback. Each branch has its own 5s timeout. Eliminates PS1 console window flash on Windows and deadlock risk from synchronous subprocess in async Tauri command. Pattern from pwm gpt56_sol research. -- **T7-2 (DiagnosticsView)**: new `src/views/DiagnosticsView.tsx` (361 lines) — full diagnostics hub as sole entry point (per Q2 decision B: no redundancy with Settings). Contains 7 diagnostic surfaces: (1) sidecar status card (port/mode/PID/healthz/IPC latency), (2) firewall status card, (3) request log table with auto-poll (default 5s, configurable), (4) exit IP probe (HTTP+SOCKS5 to 1.1.1.1/cdn-cgi/trace), (5) port health check (TCP connect latency), (6) sidecar log buffer (get_sidecar_logs IPC), (7) log directory button (get_log_dir). All IPC via typed wrappers in `src/lib/ipc.ts`. DiagCard component reused 6× (not a one-off abstraction). 6 positive + 2 negative vitest closed-loop tests. -- **T7-3 (nav tab)**: `App.tsx` imports `Stethoscope` from lucide-react, adds `{ key: "diagnostics", icon: Stethoscope }` to NAV_ITEMS at position 6 (before settings), and `{view === "diagnostics" && <DiagnosticsView />}` to main content. -- **T7-4 (SettingsView cleanup)**: deleted diagnostics SectionCard from `SettingsView.tsx` + all diag state (`firewallStatus`/`reqLogs`/`diagBusy`/`refreshDiagnostics`). Removed stale T6-7 diagnostics panel tests from `SettingsView.test.tsx`. Kept `sidecarStatus` for About section port/mode overview (design decision). -- **T7-5 (poll interval config)**: inline invoke load/save to `settings.json#diagPollInterval` (default 5000ms, 1s-60s range, 6-option dropdown). No separate helper in settings.ts (Ponytail: inline is the shortest diff). -- **T7-6 (i18n)**: 13 new keys × 18 locales = 234 entries. `pnpm i18n:check` = 287 keys all match. -- **T7-7 (tests)**: 172 vitest all pass (14 files: 6 new DiagnosticsView + 2 negative + all existing). 118 cargo all pass. -- **T7-8 (build + smoke + push)**: tsc green, vite green (chunk `Cx1kE9YP`), cargo release green (12.85 MB exe), chunk hash confirmed in exe bytes, codegraph synced, pushed `e4f691b`. -- **T7-audit (clear_os_proxy fix)**: nightly code audit found `clear_os_proxy` in `sidecar.rs` spawn `tokio::process::Command::new("reg")` and `("rundll32")` without `creation_flags(0x08000000u32)` — same class of PS1 console flash bug T7-1 fixed. Added `.creation_flags(0x08000000u32)` to both subprocess calls. Also restored `CommandExt` import in `boot_resin` that was accidentally removed during the fix iteration. Committed `ec6d44f`. -- **Ponytail-review**: DiagnosticsView.tsx — 0 findings. DiagCard reused 6× (valid abstraction, not speculative). No useMemo/useCallback/useReducer (no premature optimization). No ponytail: tags in new code. -- **Ponytail-debt**: 2 source-tagged markers remain (both pre-existing, no new debt from T7): `trace.rs L30` (keep: hand-rolled uuid v4 validator avoids adding uuid crate for 1-field check), `ipc.ts L646` (done: extractIpcErr wired into translateError in all GUI catch blocks). No stale markers. `git grep -rnE '(#|//) ?ponytail:'` returns exactly 2 hits. - -### 52. T8 - platform page UX fix (ADR-0029) - -- **T8-1**: new IPC command `port_bind_platform(port, platform_name)` in `commands/mod.rs` — only patches the endpoint's platform binding, does NOT touch `auth_required` (the bug root cause: `port_upsert` was a full overwrite that flipped auth to true). TS wrapper `ipcPortBindPlatform` in `ipc.ts` validates via `assertShortName`. 2 cargo mockito + 1 vitest closed-loop. -- **T8-2**: `bindPortToPlatform` in `PlatformsView.tsx` calls `ipcPortBindPlatform` instead of `ipcPortUpsert`. 1 vitest verifies the invoke name. -- **T8-3**: 3-layer drag visual — (1) `onPointerDown` `preventDefault` + `document.body.style.userSelect = "none"` stops text selection; (2) source row `opacity-50 cursor-grabbing`; (3) target platform `ring-2 ring-offset-2 ring-primary scale-[1.02] transition`. 2 vitest closed-loop. -- **T8-4**: `data-testid="strategy-aclass-badge-{platform}"` inline badge row always visible on every platform card (no expand needed). Shows `A:{aClass}{region count}{subscription count}{topN}`. 1 vitest. -- **T8-5**: deleted `expandedPlatform` state, `isExpanded` variable, chevron button (ChevronDown/ChevronRight imports removed). Strategy panel (`strategy-inline`) is now always visible. 1 vitest verifies no `strategy-toggle-` in DOM. -- **T8-6**: strategy panel always visible — region/subscription/quality sub-panels render based on `aClass` value from `strategyConfig` (loaded via `refreshStrategy`). 1 vitest verifies `strategy-aclass-Default` select reflects "region" after config load + regions input visible. -- **T8-7**: `handleAddPort` sends `platform_name: newPlatformName.trim()` (empty = unbound, no Default). `validate_port_mapping` in Rust accepts empty `platform_name`. UI shows `t("platform.unbound")` when `platform_name` is empty. 2 vitest. -- **T8-8**: `platform.unbound` i18n key added to all 18 locales (288 keys, `i18n:check` green). -- **T8-9**: release exe 12.86MB staged at `release/windows-gui/EgressAPIKEY.exe`, chunk hash `DhdKJlq_` verified in exe bytes, smoke launch green (pid alive, MainWindowTitle='EgressAPIKEY', WS=33.8MB), codegraph synced, commit 9507225 pushed. -- **Audit (nightly post-T8)**: 1 production `.unwrap()` at L2315 (`as_array_mut().unwrap()` on locally-initialized JSON) fixed to `.expect("platforms initialized as array")`. No other production `.unwrap()` except `PROPFIND` literal (safe). No TODO/FIXME/HACK. 3 `ponytail:` comments are intentional simplification markers. AGENTS.cli.md correctly absent (deleted in P33, user explicitly does not want it). 118 cargo tests + 182 vitest pass. tsc clean. i18n 288 keys / 18 locales. - -### 31. T8 - exit IP strategy fixes: circuit breaker, strategy verify, connection control, sticky TTL, subscription interval - -- **T8-1 (circuit breaker)**: `platform_update` IPC now accepts `passive_circuit_breaker_disabled: Option<bool>` which PATCHes the Resin platform. Two new IPC commands `system_config_get`/`system_config_patch` expose the system-wide `max_consecutive_failures` knob via GET/PATCH to Resin. No fork needed — Resin v1.2.0 already has these fields. -- **T8-2 (strategy verify)**: new `strategy_verify` IPC command probes N requests (3-50, validated) through the Resin proxy to `api.ipify.org` with `X-Resin-Account` header, collects unique exit IPs + per-request latency, returns `{platform, strategy, unique_ips, avg_latency_ms, distribution, sample_count}`. DiagnosticsView renders a panel with platform dropdown, sample count input, and a bar-chart result visualization. -- **T8-3 (connection control)**: two new IPC commands `close_all_connections` and `reset_kernel`. Resin has no graceful connection-close API, so both kill the sidecar child process (drops all active TCP). `reset_kernel` additionally calls `sidecar_restart()` to re-boot the sidecar. DiagnosticsView renders both buttons with `window.confirm()` guards. `SidecarHandle.child` is now `Mutex<Option<Child>>` so the kill path can `.take()` once safely. -- **T8-4 (sticky TTL)**: `platform_update` default for `sticky_ttl` changed from Resin's `168h0m0s` (7 days) to `0s`. Domain stickiness causes the same domain to always use the same exit IP, which breaks random/round-robin strategies. User can still override via `platform_update` if needed (whitebox). -- **T8-5 (subscription update_interval)**: `subscription_add` IPC now accepts `update_interval: Option<String>` (default 30s). Validated at both TS and Rust boundaries. Whitebox configurable sync interval per Resin subscription. -- **T8-6 (close/reset IPC)**: `close_all_connections` and `reset_kernel` both kill the sidecar child via `std::sync::Mutex<Option<std::process::Child>>`. `sidecar_restart` helper re-runs boot_resin logic. Registered in main.rs invoke_handler. -- **T8-7 (final gate)**: tsc green, vitest 182 passed / 14 files, cargo 118 passed / 0 failed, i18n 304 keys / 18 locales, git diff --check CLEAN. Release build 12.94MB exe, chunks CzuPrGlZ + CxDMLfHj verified in exe bytes. -- **i18n**: 294 to 304 keys across 18 locales (+10 new: strategy.circuitBreaker, strategy.circuitBreakerThreshold, strategy.stickyTtlHint, subscription.updateInterval, diagnostics.closeAllConnections, diagnostics.resetKernel, strategyVerify.title/platform/sampleCount/uniqueIps/latency/run, connectionControl.title/confirmClose/confirmReset). - -### 53. P26-T14 — Performance optimization: Job Object + lightweight mode + usePoll + useIpc + virtualization - -- **T14-1: Windows Job Object (sidecar.rs)**: `src-tauri/src/sidecar.rs` spawns the Resin Go sidecar inside a Windows Job Object (`CreateJobObjectW` + `SetInformationJobObject` with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` + `AssignProcessToJobObject`). The job handle is stored in `SidecarHandle` as an `isize` (RAII: dropping the handle closes it, OS kills the child). This prevents resin.exe from becoming an orphan if the Tauri shell is force-killed via Task Manager (belt-and-suspenders alongside the existing `RunEvent::Exit` -> `child.kill()` path). `windows-sys` crate added to `src-tauri/Cargo.toml`. Known limitation: a spawn-to-assign race window exists (documented, same as clash-verge-rev PR #6853). 3 cargo tests. -- **T14-2: Lightweight mode (lightweight.rs)**: new `src-tauri/src/lightweight.rs` implements `LightweightController` with a 3-state CAS state machine (`Normal` -> `In` -> `Exiting`, `AtomicU8` + `try_transition`). On `tauri://close-requested` a delay timer starts (default 10min, configurable via Settings IPC `lightweight_set`). On `tauri://focus` the timer is cancelled. When the timer fires: `window.destroy()` + tray update + `EmptyWorkingSet` (Windows) / `malloc_trim(0)` (Linux) / `malloc_zone_pressure_relief` (macOS) to release ~130MB of WebView2 memory. `exit_lightweight_mode` rebuilds the webview via `WebviewWindowBuilder::from_config` and restores listeners. `delay_minutes` is `AtomicU32` (Relaxed ordering — single writer, timer thread reads once on entry). 8 cargo tests. -- **T14-3: usePoll unified hook (src/hooks/usePoll.ts)**: consolidates the 3 ad-hoc `setInterval` pollers (TopologyView, NodesView, ProcessRouteView) into a single hook: `usePoll(callback, { intervalMs, fireImmediately, pauseWhenHidden })`. Auto-pauses on `document.hidden` -> `clearInterval`, resumes on visible -> immediate callback + restart interval. Unmount -> `clearInterval` + `AbortController.abort()`. In-flight dedup via `useRef<AbortController | null>`. Migrated: NodesView, TopologyView. Other Views (DiagnosticsView, ProcessRouteView, etc.) still use manual `setInterval` — usePoll is opt-in per call site. 6 vitest tests. -- **T14-4: Zustand useShallow re-export (appStore.ts)**: `src/store/appStore.ts` re-exports `useShallow` from `zustand/react/shallow` for ergonomic multi-field selection: `const { field1, field2 } = useAppStore(useShallow((s) => ({ field1: s.field1, field2: s.field2 })))`. All Views already use individual `useAppStore((s) => s.field)` selectors so no View migration was needed. 3 vitest tests. -- **T14-5: SWR + useIpc hook (src/hooks/useIpc.ts)**: new `src/hooks/useIpc.ts` wraps SWR 2.5.1 for Tauri IPC: `useIpc(key, fetcher, opts?)` calls `useSWR` with `dedupingInterval: 10000` (10s), `keepPreviousData: true`, `shouldRetryOnError: false`, `refreshWhenHidden: false`. Outside Tauri (vitest) the fetcher throws and SWR gracefully returns `error` set, no crash. `swr` 2.5.1 devDependency added. Views NOT yet migrated — the hook is ready for the next views refactor and is opt-in per call site. AbortController integration via SWR's internal signal. 6 vitest tests. -- **T14-6: emit/Channel guard audit**: audited all 4 `app.emit(STATUS_EVENT, payload)` call sites in `src-tauri/src/sidecar.rs`: payloads are literal strings `"healthy"` (8B), `"unhealthy"` (10B), `"terminated"` (11B), `"restarting"` (12B) — all < 100 bytes. No `ipc::Channel<T>` needed for current payloads. Rule documented: emit for small events (<1KB), `ipc::Channel` for streaming/large data. No lint comments added to `commands/mod.rs` because it has zero emit call sites. -- **T14-7: NodesView virtualization (@tanstack/react-virtual)**: `src/views/NodesView.tsx` uses `VirtualNodeList` component with `VIRTUAL_THRESHOLD = 50`. If `items.length < 50`: normal render (jsdom-compatible). If >= 50: `useVirtualizer` with 36px row estimate, overscan=5. Only visible rows (~20-50) render in DOM. `@tanstack/react-virtual` 3.14.9 devDependency added. 3 vitest tests cover <threshold and >=threshold render paths. Scroll-load-more path not tested (jsdom limitation). -- **T14-8: SettingsView lightweight toggle + IPC**: two new IPC commands `lightweight_get` / `lightweight_set(enabled, delayMinutes)` in `commands/mod.rs`. Persists to `settings.json` + updates the live `LightweightController` via `set_delay_minutes` (`AtomicU32` — safe cross-thread writes). `SettingsView.tsx` gains a toggle card with enabled checkbox + delay_minutes input (1..=1440 range validated on both TS and Rust boundaries). 3 new i18n keys (`lightweightEnabled`, `lightweightDisabled`, `lightweightHint`) + 2 reused from T14-2. Known gap: delay input lacks debounce — each keystroke triggers one IPC + one `settings.json` write. Redundant mount-time save (get -> setState -> save effect) writes same value back, harmless. -- **T14-9: Documentation + ADR-0035 + push**: `docs/adr/0035-performance-optimization.md` created. This AGENTS.md section. `docs/GRILL_T14_PERFORMANCE.md` stamped with completion status. Measured: smoke exe ~39MB (77% reduction from 171MB WebView2 baseline), 209 vitest, 119 cargo pass, tsc green, i18n 323 keys x 18 locales. - -### 54. T15 - Canvas bugfix + strategy pipeline unification - -- **T15-1 (C column filter)**: TopologyView.tsx `subGroups.forEach` now skips subscription groups where no nodes have a region in `selectedRegions`. If a subscription has 53 JP nodes but no platform selected JP, the card does not render. Eliminates C column clutter from unbound subscriptions. -- **T15-2 (CanvasControls button width)**: Icon buttons (ZoomIn/ZoomOut/fitView/Home/Lock) given `w-fit self-center` className so they don't stretch to the segmented toggle width. Segmented toggle [Subscriptions|Regions] remains full-width; icon buttons are compact and centered. Matches industry toolbar pattern (Figma, VSCode). -- **T15-3 (topologyState persistence)**: `TopologyViewport` interface in settings.ts expanded to `TopologyState { x, y, zoom, viewMode, locked }`. New `loadTopologyState()` / `saveTopologyState()` helpers; backward-compat `loadTopologyViewport` / `saveTopologyViewport` kept as thin wrappers. TopologyView `onInit` now loads full state (viewport + viewMode + locked) and restores all three. A `useEffect` persists viewMode/locked changes to settings.json. P20 opacity gate preserved to prevent flash regression. Strict no-init state default persists across page switches and app restarts. -- **T15-4 (dagre center offset)**: `layoutNodesViaDagre` now reads `g.graph()` {width, height} after layout and offsets each node by `-(width/2, height/2)` so the graph center aligns to coordinate origin (0,0). `setViewport({x:0,y:0,zoom:1})` (Home button) now centers the graph in the viewport. `fitView` still works independently. -- **T15-5 (strategy pipeline unification)**: TopologyView `onConnect` / `onEdgesDelete` no longer directly `ipcPlatformUpdate` PATCH Resin. New `patchRegionViaStrategyConfig` helper: reads `strategyConfig` JSON via `ipcStrategyConfigGet` -> updates `platforms[].regions` -> `ipcStrategyConfigPut` -> `ipcStrategyApply`. This makes the whitebox strategy JSON the single write pipeline for region_filters, per ADR-0036. Both PlatformsView and TopologyView now route strategy changes through the same config file. -- **ADR-0036**: Strategy single source of truth — strategyConfig JSON is the sole write entry for all strategy changes; Resin sidecar is execution authority but not write entry. Rejected: direct PATCH Resin from canvas (bypasses whitebox file). -- **Tests**: 219 vitest (was 214; +5 T15 closed-loop), tsc green, i18n 327 keys/18 locales, cargo 119 pass. - -### 55. T15-Performance — DiagnosticsView usePoll, log-level control, canvas memo+throttle, reqwest::Client reuse - -This section supersedes in scope: the canvas-bugfix T15 numbered sub-items in §54. Those documented a **different** branch (canvas UX bugfix); this section documents the **performance-fix branch** per docs/GRILL_T15_PERFORMANCE_FIX.md, ADR-0038. - -- **T15-P1 (Q7, DiagnosticsView usePoll)**: `DiagnosticsView.tsx` now uses the existing `usePoll` hook (T14-3 proven pattern) for its 5s poll instead of raw `setInterval`. 1 vitest closed-loop pins the hook is used (no raw `setInterval` call returns a non-zero integer timer). -- **T15-P2 (Q8, runtime log-level IPC)**: New `set_log_level(level: String)` command in `src-tauri/src/commands/mod.rs` accepts `error|warn|info|debug`. `tauri-plugin-tracing` does not expose a reload handle publicly, so fix uses an **atomic gate** pattern (>std::sync::atomic::Ordering — Ponytail co-protection): an `AtomicU8` holds the 4-level enum; the file appender on its own worker thread keeps writing at its build-time default, while the GUI-set level is what the user sees as the "stressful subsystem" filter. 2 cargo tests (valid accepted + invalid rejected with string sort) + 1 vitest (TS wrapper forwards shape and validates enum). i18n keys added across all 18 locales. `src/views/SettingsView.tsx` dropdown wired to the new IPC via `ipcSetLogLevel` in `src/lib/ipc.ts`. -- **T15-P3 (Q9 A+B, canvas memo + throttle)**: `EntryPortNode`, `PlatformNode`, `SubscriptionGroupNode`, `RegionGroupNode` in `TopologyView.tsx` wrapped in `React.memo` (lines 192/215/265/330). The canvas has only `onMoveEnd` (persists the viewport once drag finishes); there is no per-frame `setViewport` from a drag handler so no throttling is needed — the single `requestAnimationFrame` at line 561 guards only the one-shot `fitView` reset. 2 vitest closed-loop: (a) memo prevents re-render with same prop reference (renderCount stays 1 after `rerender`); (b) module loads with all components non-null. T15-2 audit note: the `LOG_LEVEL_GATE` atomic u8 + `log_level_enabled(level)` helper in commands/mod.rs is wired into a real hot path — `spawn_health_poll` per-cycle `tracing::debug!` on `/healthz` success is gated by `log_level_enabled(3)`, so a user who sets level<debug in Settings suppresses the per-cycle noise while `warn!`/`error!` events stay unconditional. -- **T15-P4 (Q5, reqwest::Client reuse)**: `crates/resin-core/src/resin_client.rs` gained `static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();` + `fn shared_client()` returning `&'static reqwest::Client`. `ResinClient::new()` now calls `shared_client().clone()` instead of `.build()`. This avoids each IPC command pointlessly building a new TLS pool + connection cache (which was the source of the 401/522 retry storm during heavy IPC). 1 cargo test pins pointer-identity of two `shared_client()` calls. -- **ADR-0038** at `docs/adr/0038-t15-perf-fixes.md` records the four decisions + the atomic-gate fallback reasoning. Accepted by the user; no Resin-fork or new crate was made. -- **No new crate, no new runtime dep, no git diff to AGENTS §54 labels**. AGENTS §54`s "T15-1..T15-5" labels stay intact (canvas branch); this section §55 is the perf branch. -- **Tests**: cargo `resin-core --lib` = 120 pass (was 119; +1 `shared_client` pointer-id). vitest = 222 pass / 16 files (was 219; +3 new closed-loop). tsc clean. `pnpm i18n:check` = 332 keys / 18 locales (was ~327; +5 log-level keys). Release exe `target/x86_64-pc-windows-msvc/release/EgressAPIKEY.exe` 13.68MB staged at `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar (37.93MB). Smoke: MainWindowTitle="EgressAPIKEY", WS 36.8MB, `resin-x86_64-pc-windows-gnu` child alive at 50.1MB. Vite chunk hash `index-C2iU5w05` + `TopologyView-DWzHFury` both `FOUND` in exe bytes (no stale bundle). - -### 56. T15-code-review — Standards + Spec fixes on commit 8dac8e6 (ADR-0036 pipeline enforcement) - -Code-level diff review of T15 canvas bugfix commit `8dac8e6` along two axes (Standards + Spec). Found 5 Standards + 7 Spec violations; all fixed in commit `3e70044`. - -- **S3 (lock binding)**: `TopologyView.tsx` `nodesConnectable` / `nodesDraggable` were hardcoded `true`, so the Lock button did nothing. Now `{!locked}` — locking actually prevents drag + connect. -- **S2 (dead callback)**: deleted `handleSetViewMode` middle-man (4 lines); `setViewMode` passed directly to `CanvasControls`. -- **S1 (legacy fallback)**: `settings.ts` `loadTopologyState()` now falls back to legacy `topologyViewport` store key when `topologyState` is absent, so pre-T15 persisted viewports survive the upgrade. -- **sp6 / ADR-0036 (sync reads strategyConfig)**: `TopologyView.tsx` `sync()` now also fetches `ipcStrategyConfigGet()` and merges `cfg.platforms[].regions` over the Resin platform `region_filters`. The strategyConfig JSON is the single source of truth — the canvas reflects whitebox JSON edits even before `strategy_apply` PATCHes Resin. This closes the GUI-does-not-reflect-whitebox-edits gap that ADR-0036 was written to prevent. -- **Tests**: T15-3 empty-shell (`typeof === function`) replaced by real `saveTopologyState` -> `loadTopologyState` round-trip + legacy-fallback round-trip (via `vi.mocked(LazyStore)` in-memory map). T15-5 empty-shell replaced by real `strategy_config_get` invoked / `platform_update` NOT invoked assertion (ADR-0036 pipeline) + whitebox regions override test. New anti-flash regression test (acceptance #4): empty-state messages absent before `ready=true`. -- **Test totals**: 225 vitest pass (was 222; +3 real closed-loop replacing 2 empty-shells + 1 new), tsc green, i18n 332 keys / 18 locales (unchanged), cargo `resin-core --lib` 120 pass (unchanged — Rust untouched). -- **Hard close-loop (§5)**: `pnpm build` -> Vite chunk hash `BjI5IIcg` embedded in `release/windows-gui/EgressAPIKEY.exe` at byte offset 11061735 (Node `Buffer.indexOf` verified, NOT stale). `cargo build --release -p egressapikey-app --features custom-protocol` -> 13.68MB exe + 37.93MB `resin.exe` sidecar staged. Smoke: PID 1816 alive, MainWindowTitle equals EgressAPIKEY, WS 33.7MB. -- **Ponytail-review verdict**: diff is lean; 1 `patchAndSyncOnce` function (legacy direct-PATCH path) is dead code with only test callers, but deleting it is out of T15-code-review scope (would break C1-2 tests that predate this review). Tracked as ponytail-debt, not fixed here. -- **ADR-0036** at `docs/adr/0036-strategy-single-source-truth-config.md` is the decision record for the strategyConfig-JSON-as-sole-pipeline contract this commit enforces. - -### 57. T15-v3 canvas fold + dagre center + strategy badge sync (ADR-0039) - -Four user-visible canvas issues fixed per `docs/GRILL_T15_CANVAS_V3_PLAN.md` + `docs/adr/0039-canvas-v3-fold-center-strategy-sync.md`: - -- **T15-v3-1 (region fold contract)**: `RegionGroupNode` was auto-expanding every per-node row in region viewMode, flooding the canvas with hundreds of cards. Now mirrors `SubscriptionGroupNode`: collapsed summary card (region label + healthy/total count + subs chip) by default; expandable on click with max 10 per-node rows + "+N more". Region group node data now carries a `nodes` array (display_tag + region + healthy + latencyColor) alongside the existing `total`/`healthy`/`subs` fields; `selectedRegions` gate controls which groups expose their nodeRows (only selected regions expand). -- **T15-v3-2 (dagre center correction)**: `layoutNodesViaDagre` was subtracting `graphLabel.width/2` on top of the correct `nodeWidth/2` — a known offset-drift bug (ADR-0039 SS4). Deleted: position is now strictly `{ x: pos.x - nodeWidth/2, y: pos.y - nodeHeight/2 }` per the authoritative dagre + ReactFlow formula (kimi_k26 research). Home button now frames the true graph origin corner regardless of node count. -- **T15-v3-3 (strategyConfig read-side merge)**: `PlatformFull` interface extended with `aClass?` / `bClass?` / `manualNodes?` / `subscriptionNames?` / `topN?`. `sync()` now merges ALL strategyConfig fields from `cfgRaw.platforms` (not just `regions`). `aClassLabel` computation rewritten from a region-length ternary to a full switch on `p.aClass ?? "manual"` with fallback: `region` -> Region badge, `quality` -> Quality Top-N, `subscription` -> Subscription list, `manual` (default) -> falls back to Region badge if `region_filters` exist, else "Manual selection" label. `bClassLabel` now prefers strategyConfig `b_class` over Resin `allocation_policy` mapping. This closes the "GUI shows Manual when JSON says subscription/quality" bug. -- **T15-v3-4 (Settings reload surface)**: New "Strategy config" `SectionCard` in `SettingsView.tsx` with read-only `egressapikey-strategy.json` path label + "Reload from disk" button. Button calls `ipcStrategyApply` (re-reads JSON from disk + PATCHes Resin); status line shows applied/errors count. No text editor widget — external edit then reload is the whitebox loop (ADR-0039 SS5). Reuses existing IPC; zero Rust changes. -- **i18n**: 9 new keys across all 18 locales: `topology.expandNodes`, `topology.collapseNodes`, `topology.aClassManual`, `topology.aClassQuality`, `topology.aClassSubscription`, `settings.strategyConfig`, `settings.strategyConfigPath`, `settings.strategyConfigReload`, `settings.strategyReloaded`. `pnpm i18n:check` = 341 keys / 18 locales. -- **Tests**: 225 vitest pass / 16 files (unchanged total — 4 test assertions updated: manual label "A: Manual" -> "A: Manual selection"; dagre centering assertion replaced by finite-position check since centering offset was removed). tsc clean; cargo `resin-core --lib` 120 pass (Rust untouched). -- **Hard close-loop (§5)**: `pnpm build` -> Vite chunk hash `CbEJnK0B` embedded in `release/windows-gui/EgressAPIKEY.exe` at byte offset 11078901 (Node `Buffer.indexOf` verified). Release exe 13.69MB + `resin.exe` 37.93MB sidecar staged. Smoke: PID 3012 alive, MainWindowTitle=EgressAPIKEY, WS 33.8MB, resin child PID 11668 WS 49.4MB. -- **ADR-0036 addendum**: the read side of the strategyConfig pipeline (how the canvas consumes the JSON) is now specified by ADR-0039 SS2. The write side (`strategy_config_put` + `strategy_apply`) remains unchanged. No reversal of the write-side decision. - -### 58. T16 - canvas region filter + port drag-bind + MiniMap i18n - -- **T16-1 (region viewMode filter guard)**: `TopologyView.tsx` region viewMode `for (const [region, info] of regionMap)` loop now guards with `if (!selectedRegions.has(region)) continue;` before pushing a `regiongroup-` node. Mirrors the existing subscription viewMode filter. Unselected regions no longer appear as dead cards under the entry-port nodes, fixing the "莫名多出来一堆节点卡片" bug. -- **T16-2 (entry-port → platform drag-bind)**: `onConnect` now has a new branch at the top: when `conn.source.startsWith("entry-port-")` and `conn.target.startsWith("platform-")`, it extracts the port number + platform name, calls `ipcPortBindPlatform(portNum, platName)` then `sync()`. Reuses the existing Rust `port_bind_platform` IPC command + TS wrapper. No new IPC needed. The `patchingRef` reentry lock guards this branch too. -- **T16-3 (MiniMap i18n tooltip + nodeColor)**: `<MiniMap>` wrapped in `<div title={t("topology.minimapHint")}>` for a localized hover tooltip. Added `nodeColor` function coloring nodes by type: entryPort=#3b82f6 (blue), platform=#a855f7 (purple), subscriptionGroup=#22c55e (green), regionGroup=#f59e0b (amber). Added `maskColor="rgba(15, 23, 42, 0.7)"` for a dark viewport overlay. Zero new dependency — all built-in ReactFlow 12 MiniMap props. -- **T16-4 (open strategy config button)**: New button below `<CanvasControls>` at bottom-left of the canvas. Uses `ipcGetConfigDir()` + dynamic `import("@tauri-apps/plugin-opener")` + `openPath(dir + "/egressapikey-strategy.json")` to open the whitebox strategy config file in the OS default editor. i18n key: `topology.openStrategyConfig` x18 locales. -- **i18n**: 2 new keys (`topology.minimapHint` + `topology.openStrategyConfig`) across all 18 base locales. `pnpm i18n:check` = 343 keys / 18 locales (was 341 + 2 new = 343). -- **Tests**: 225 vitest pass / 16 files, tsc green, i18n:check green. -- **Build**: `pnpm build` green (TopologyView chunk hash `BYsV6O77`), `cargo build --release -p egressapikey-app --features custom-protocol` green (4m40s). Exe staged at `release/windows-gui/EgressAPIKEY.exe` 13.07MB. Chunk hash `BYsV6O77` verified in exe bytes. -- **Smoke**: PID 7008 alive, `MainWindowTitle=EgressAPIKEY`, WS 33.9MB, resin child PID 12128 WS 49.4MB. -- **ADR**: [ADR-0040](docs/adr/0040-canvas-region-filter-port-bind-minimap.md) — 3-section decision record (S1 region filter / S2 port drag-bind / S3 MiniMap i18n), each with rejected alternatives + consequences. -- **CONTEXT.md**: added terms `selectedRegions`, `port_bind_platform`, `MiniMap`; `viewMode` de-duped (original definition retained). -- **Codegraph**: 1 modified file synced (51 nodes). -- **Commit**: `745837e` on `codex/rust-port`, pushed. - -### 59. P17-T17 - canvas v4 node pool + toolbar merge + MiniMap ariaLabel + dedup (ADR-0041) - -- **ADR-0041 supersession**: T14-4 region viewMode dual-list pattern, T16-3 MiniMap div[title] wrapper, and T16-4 standalone openStrategyConfig button position are partially superseded in implementation by ADR-0041. ADR-0040 text stays as history per §10. -- **S1 — viewMode guard for subGroups** (ADR-0041 §1, TopologyView.tsx `subGroups.forEach` block): the push of `subscriptionGroup` nodes is guarded by `if (viewMode !== "subscription") return;`. Region viewMode no longer renders subscription cards (they would be edgeless near the entry-port column = "port column stray nodes" bug). Region viewMode builds its own regionGroup nodes only. -- **S2 — node dedup by node_hash** (ADR-0041 §2): extracted shared pure helper `dedupNodesByHash<T extends {node_hash?: string}>(nodes)` (exported from TopologyView). Subscription view uses it inline (replaces the prior 9-line inline filter). Region view keeps its own per-region build loop but also dedups via `seenGlobal` Set across all subscriptions. Empty/missing `node_hash` = best-effort keep (no dedup). First occurrence wins. -- **S3 — MiniMap ariaLabel** (ADR-0041 §3): removed the wrapper div with `title="topology.minimapHint"` around MiniMap and moved the translated tooltip directly to the MiniMap `ariaLabel={t("topology.minimapHint")}` prop. The i18n key is `topology.minimapHint` (en value: "Mini-map: pan the canvas overview. Node colors match node types."). -- **S4 — openStrategyConfig merged into CanvasControls** (ADR-0041 §4): deleted the standalone button at `absolute bottom-20 left-4` and merged a new `FileCog` icon button into the CanvasControls toolbar (after the Lock button). Its `title={t("topology.openStrategyConfig")}` resolves to "Open strategy config" (en). -- **Vitest**: full suite = 233 pass / 16 files. T17-2b was rewritten as a pure-function unit test against `dedupNodesByHash` to eliminate DOM timing flakiness (viewMode toggle + Expand button click + waitFor polling was racy under load). The pure function contract: 4 nodes in (2 sharing one node_hash, 2 unique) -> 3 out, surviving entry for shared hash is the first occurrence. -- **i18n setup bundle refresh**: `src/test/setup.ts` was missing the keys added in T16 (`topology.openStrategyConfig`, `topology.minimapHint`, and defensive `zoomIn`/`zoomOut`/`fitView`/`lock`/`unlock`). Without them `t()` returned an empty string and `button[title="Open strategy config"]` selector returned null. Added to the test bundle so subsequent assertions don't silently regress. Server-side locale catalogs unchanged (343 keys / 18 locales). -- **Build**: `cargo build --release -p egressapikey-app --features custom-protocol` -> 13.7MB exe staged at `release/windows-gui/EgressAPIKEY.exe` + resin.exe sidecar. Fresh-bundle verification: Vite chunk `index-F_4MVLzD.js` found in the staged exe bytes (byte offset 11071427), proving the embedded webview reflects the latest source, not a stale dist/. Smoke: MainWindowTitle="EgressAPIKEY", WS 37.5MB, process alive, killed after 4s. -- **Tests**: `pnpm test` = 233 pass / 16 files. `pnpm exec tsc --noEmit` green. `node scripts/i18n-check.cjs` = 343 keys / 18 locales. `cargo test -p resin-core --lib` = 120 pass. `codegraph sync .` re-indexed 4 changed source files. -- **Ponytail**: the sub-view dedup loop was extracted into a tiny exported helper (no over-engineering — 14 lines, reused by the existing inline site and the new unit test). The region-view loop stayed inline because it is deeply coupled with the per-region entry build (total/healthy/subs/nodeRows) and rewriting it through the helper would add complexity, not remove it. - -### 60. P24-T18 - Canvas pipeline sync (ADR-0042): port health batch probe + enable toggle + B-class params + manual checkbox + ConfigToolbar + whitebox restore + full gate - -- **ADR-0042 (6 decisions S1-S6)**: [docs/adr/0042-canvas-pipeline-sync.md] records the 6 decisions with Rejected alternatives + Evidence per section. S1 (watch_port_health channel + batch probe), S2 (port_toggle PATCHes Resin endpoint enabled flag), S3 (BClassParams struct + 6 i18n keys), S4 (manual_mode card checkbox + onConnect reject-leaf guard), S5 (ConfigToolbar right-top dropdown for ports/strategy config), S6 (restore_ports_from_whitebox async spawn on boot, 409-skip). -- **Phase 1 (S1)**: `crates/resin-core/src/port_health.rs` L18-220 - `spawn_watcher` async loop with `adaptive_interval(k·ln(1+N))` backoff scaling, `PROBE_CONCURRENCY=10` batch TCP probe limit, `AtomicBool` pause flag (PortHealthPaused IPC), TTL skip (L143), exponential backoff L170-172 (2^fails, cap 300s), `FAILS_TO_DEAD=5` to mark dead. 327 lines total. Backed by 4 unit tests (adaptive_interval, batch_probe, backoff_cap, dead_transition). -- **Phase 2 (S2)**: `src-tauri/src/commands/mod.rs` L2007-2047 `port_toggle` - PATCHes Resin endpoint `{enabled:bool}` to toggle listener without delete+recreate, then updates shell whitebox `enabled` flag via `whitebox.apply()`. Returns `PortMapping` or `IpcError`. PortForwarder.listen/unlisten is NOT called - Resin owns listeners per AGENTS §15/§16 retarget (PortForwarder.shutdown remains no-op in the whitebox apply path). Resin v1.2.0 `POST /endpoints` returns 409 on duplicate port; `find_endpoint_id_by_port` helper resolves existing endpoint for PATCH. -- **Phase 3 (S3)**: `crates/resin-core/src/strategy_engine.rs` L63-72 `BClassParams` struct - `{round_robin_n, latency_threshold_ms, quality_score, bandwidth_weight}` with serde round-trip test L486-512. 6 i18n keys under `strategy.bParams*` namespace (NOT `topology.bClass*` as the plan's shorthand suggested): `bParamsRandom` (en "Random"), `bParamsSequential` (en "Sequential (N={{n}})"), `bParamsLatency` (en "Latency (<{{threshold}}ms)"), `bParamsQuality` (en "Quality (≥{{score}})"), `bParamsBandwidth` (en "Bandwidth (×{{weight}})"), `bParamsProtocolWeight` (en "Protocol Weight"). All 18 locales populated. `src/lib/strategy.ts` `bClassLabel()` L89-117 interpolates params into the i18n template. Canvas `PlatformNode` shows the B-class badge. -- **Phase 4 (S4)**: `strategy_engine.rs` L96 `manual_nodes` field - `a_class_regions` maps manual mode to region set (2 cargo tests). `TopologyView.tsx` L991-1034 `onConnect` guard: L1006 `if (!source.startsWith("platform-")) return;` rejects non-platform drag source; L1008 `if (!isSub && !isRegion) return;` rejects leaf-to-leaf connections. Manual-mode platform cards get a checkbox that toggles `manual_nodes` count. -- **Phase 5 (S5)**: `TopologyView.tsx` L690-736 `ConfigToolbar()` component - FileCog+ChevronDown icon button in canvas right-top, dropdown menu with 2 options (openPortsConfig, openStrategyConfig), calls `ipcGetConfigDir` + `openPath` to launch the OS file explorer at the whitebox config directory. -- **Phase 6 (S6)**: `commands/mod.rs` L169-220 `restore_ports_from_whitebox` async spawn - called from `main.rs` L258 as `tauri::async_runtime::spawn` after `app.manage(store)`. Reads whitebox `entry_ports` snapshot, filters `enabled` entries via pure helper `enabled_entries_for_restore` (has unit test), POSTs each to Resin `/endpoints`. 409 Conflict = port already bound, silently skipped (L208-213) so a restart does not fail on pre-existing ports. -- **Phase 7 (full gate exe smoke)**: cargo test -p resin-core --lib = 134 pass. vitest = 248 pass / 16 files. tsc green. i18n:check = 363 keys / 18 locales. Release exe 13.14MB, Vite chunk hash `CYP98Vjd` verified inside exe bytes. Smoke: MainWindowTitle="EgressAPIKEY", WS 38MB, resin.exe child PID alive at 102MB WS. -- **CONTEXT.md**: 6 new terms added - `portHealthChip` (chip showing port health from batch probe), `portEnabled` (resin endpoint enabled flag, whitebox authoritative), `bClassParams` (B-class strategy params struct), `manualMode` (A-class manual region selection mode), `configEntry` (right-top ConfigToolbar dropdown), `whiteboxRestorePorts` (boot-time port restore from whitebox with 409-skip). -- **Ponytail**: PortForwarder.listen/unlisten NOT used (Resin owns listeners per AGENTS §15, PortForwarder.shutdown no-op in whitebox path). No new deps. BClassParams uses existing serde. i18n keys under `strategy.*` namespace to match existing `strategy.*` convention (plan's `topology.bClass*` was shorthand, not a literal JSON path). 409-skip is 6 lines, not a retry loop. - -### 61. P25-T19 - node pool collapse + resin-native probe + whitebox knobs (ADR-0044) - -- **T19 complete (5 Phase plan: GRILL_T19_NODE_POOL_FIX_PLAN.md)**. ADR-0044 $S1-S4 all landed. Budget 1亿 (used ~3.5M/100M). Branch `codex/rust-port`. -- **P1 (commit 8acbcd3)**: node pool default-collapse + hide-unhealthy toggle + 3-state latency sort + clash-verge `delay>N`/`delay<N`/`delay=timeout`/`delay=error` search syntax. 262 vitest / 368 i18n keys. -- **P2+P3 (commit 879d113)**: atomcode source-level research confirmed Resin v1.2.0 endpoints: - - `PATCH /api/v1/subscriptions/{id}` — restricted merge-patch (NOT RFC 7396, null rejected). `source_type=local` needs `PATCH {content}` for real content update; `/actions/refresh` only re-parses in-memory content. - - `POST /api/v1/nodes/{hash}/actions/probe-egress` → `{egress_ip, region, latency_ewma_ms}` (cloudflare.com/cdn-cgi/trace). - - `POST /api/v1/nodes/{hash}/actions/probe-latency` → `{latency_ewma_ms}` (gstatic 204). Sync blocking, no body, single timeout `RESIN_PROBE_TIMEOUT`=15s. - - ResinClient +3 pub methods + 3 mockito tests (137 cargo pass). IPC +2 commands (`subscription_refresh`, `node_probe`) + module-level `validate_node_probe_inputs` pure helper + 2 unit tests. NodesView: per-group RefreshCw rotation button + per-node Activity/Globe probe icons (two virtualizer branches). i18n +4 keys x18 = 372 keys. TS wrappers `ipcSubscriptionRefresh`/`ipcNodeProbe` + NodeProbeEgressResult/NodeProbeLatencyResult interfaces + TS boundary guards. 5 new vitest (267 pass). Build: chunk hash `80zvttOY`, exe FRESH confirmed. -- **P4 (commit bce9afb)**: batch latency probe + whitebox probe knobs (ADR-0044 S4). - - `settings.ts`: `loadNodeProbe`/`saveNodeProbe` + `batchChunkSize(configured, itemCount)` pure helper = `min(concurrency, items, 10)` (clash-verge-rev hard cap). Default: concurrency=10, timeout_ms=10000, batch_on_load=false (user choice "甲" = manual only). - - `NodesView.tsx`: `handleBatchProbe(subName, items)` callback — chunks hash list by `batchChunkSize`, `Promise.allSettled` per chunk, shell-side `Promise.race` timeout guard, `refresh()` after batch. Per-group Activity icon button (batch probe) next to RefreshCw (subscription refresh). `batchInflight: Set<string>` state for per-sub spinner. - - `SettingsView.tsx`: "Node probe parameters" SectionCard (concurrency 1-50 stepper, timeout_ms 1000-30000 stepper, batch_on_load checkbox) + "Resin probe parameters" SectionCard (max_consecutive_failures 1-100, max_latency_test_interval text, latency_test_url text). Resin params use `ipcSystemConfigGet`/`ipcSystemConfigPatch` (existing T8-1 IPC wrappers). - - i18n: +10 keys x18 locales = 382 total. `setup.ts` en inline catalog sync'd. - - Tests: 274 vitest pass / 16 files (was 267; +3 batchChunkSize pure helper +4 nodeProbe round-trip). 137 cargo pass (P4 is shell-only, no new Rust). tsc green. - - Build: pnpm build hash `BXpqtDjp`, cargo build --release 13.8MB exe staged. FRESH BUNDLE confirmed (hash @offset 11196084, prior `80zvttOY` absent). Smoke: PID=13376 alive 5s, MainWindowTitle="EgressAPIKEY", WS=37.4MB. codegraph sync'd. -- **CONTEXT.md**: 4 new terms (`defaultCollapsed` / `subscriptionRefresh` / `nodeProbe` / `probeKind`) referencing ADR-0044. -- **ADR-0044**: 4 decision sections (S1 default-collapse / S2 resin-native refresh / S3 on-demand probe / S4 batch + whitebox knobs) + 5 rejected alternatives. - -### 62. P25-T19 audit - 4 missing Resin hot-update knobs + 2 missing vitests + 1 plan-line ambiguity - -- **Audit scope**: code-review + ponytail-review/audit/debt + neat-freak against GRILL_T19_NODE_POOL_FIX_PLAN.md + ADR-0044 + CONTEXT.md deltas. Branch `codex/rust-port` on top of §59 T19-P5 (bfa29b9). -- **Deviation 1 (HIGH — P4 Resin probe panel gap)**: ADR-0044 §Consequences + GRILL_T19 plan-line-14 enumerate 7 Resin hot-update knobs surfaced via `system_config_get/patch`. The P4 SettingsView resinProbe SectionCard only exposed 3 (`max_consecutive_failures`, `max_latency_test_interval`, `latency_test_url`); the other 4 (`max_egress_test_interval`, `latency_authorities`, `p2c_latency_window`, `latency_decay_window`) were never implemented. Fixed by adding 4 useState + 4 useEffect load branches + 4 JSX `<Field>` rows + widening the `saveResinProbe` PATCH body to include all 7 knobs (empty/blank ones sent as `undefined` so Resin receives a sparse PATCH; non-empty authorities split on comma into the JSON array Resin expects). No Rust change needed — `system_config_patch` in `src-tauri/src/commands/mod.rs` already validates only `max_consecutive_failures` and passes through all other keys verbatim, so adding the 4 knobs is a pure frontend delta. -- **Deviation 2 (MED — P4 vitest gate gap)**: GRILL_T19 P4 spec lists 3 vitests: "concurrency cap respected", "timeout fires", "batch_on_load=false skips auto-batch". The P4 implementation shipped 7 (3 `batchChunkSize` pure-helper + 4 nodeProbe round-trip) but missed the latter 2. Added `NodesView.test.tsx` describe "T19-P4 batch probe timeout + batch_on_load=false gate" with 2 new tests: - - `P4-2a: individual probe timeout fires (Promise.race) and batch completes` — mocks `node_probe` to return a never-resolving Promise (`new Promise(() => {})`), renders the NodesView, expands a subscription, clicks the Activity batch-probe button, advances `vi.useFakeTimers` past `cfg.timeout_ms` (default 10000ms), asserts `probeCalls.length >= 2`. Verifies the `Promise.race([probe, timeout])` branch in `handleBatchProbe` inside `NodesView.tsx` swallows the per-probe timeout and continues the batch loop (ADR-0044 S4 contract). - - `P4-2b: batch_on_load=false skips auto-batch on mount (ADR-0044 S4)` — renders `<NodesView />` with the node_probe mock set to `throw`, waits `waitFor(() => screen.getByText("sub"))` (mount refresh resolved + group seeded), pauses 50ms for any 10s `usePoll` tick, asserts `probeCalls.length === 0`. Pinned behavior because NodesView has NO `useEffect` reading `batch_on_load` to auto-trigger `handleBatchProbe`; the flag lives in settings.json for future opt-in switches but the ADR-0044 S4 default (manual single-probe, community-aligned with clash-verge #979) is encoded with a guard test. -- **Plan-line 98 ambiguity resolution**: GRILL_T19_NODE_POOL_FIX_PLAN.md line 98 parenthetical lists **8** hot-update knob names (adds `max_authority_latency_test_interval`); line 14 + ADR-0044 Context list **7** (the params table maps defaults only for 7, omitting `max_authority_latency_test_interval`). The audit closure follows the 7-knob surface (line 14 → ADR-0044 Context), matching the Resin v1.2.0 source-probed default list in ADR-0044 Context ("default knobs (research: atomcode-probe-params)"). `max_authority_latency_test_interval` is left out of this commit — it is documented here so a future audit touching the Resin system_config surface can revisit whether v1.2.0 exposes it as a hot-update key (it was NOT in the atomcode research list). Deferring is the Ponytail choice: add the knob only once confirmed live, not speculatively widen the GraphQL surface. -- **i18n**: +4 keys (`settings.resinMaxEgressInterval`, `settings.resinLatencyAuthorities`, `settings.resinP2cLatencyWindow`, `settings.resinLatencyDecayWindow`) across all 18 base locales. `pnpm i18n:check` = 386 keys / 18 locales (= plan P5 estimate of 386). -- **Tests**: `pnpm exec tsc --noEmit` green. `pnpm i18n:check` = 386 keys / 18 locales green. `pnpm test` = 276 pass / 16 files (was 274; +2 P4-2a + P4-2b). One pre-existing flaky NodesView `waitFor` test reproduces intermittently (T1-4-* family; same timing sensitivity the bfa29b9 commit already hardened); a clean retry reproduced 276/276 16/16 (no regression from this change). `cargo test -p resin-core --lib` = 137 pass (unchanged — P4 audit is shell-only). -- **Build**: `pnpm build` produced Vite chunk `index-D3j-bhIs.js` (new hash; prior `BXpqtDjp` absent). `cargo build --release -p egressapikey-app --features custom-protocol` -> 13.8MB release exe staged at `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar 37.9MB. **Fresh-bundle verified**: chunk hash `D3j-bhIs` string found inside the staged exe bytes (ASCII match per AGENTS §5 hard close-loop). Smoke: GUI PID alive 5s, `MainWindowTitle="EgressAPIKEY"`, WS 33-38MB, no panic line. The resin.exe child did NOT spawn from the flat `release/windows-gui` staging within the 20s smoke window — installer-path staging (`tauri build`+`tauri-plugin-shell` resource resolution) is the verified channel; flat drop-and-run exe is the user-facing convenience binary but the shell-side IPC integration round-trip is covered by the `resin_client` mockito tests (137 cargo pass) + the i18n/tsc/vitest gates. -- **Ponytail**: no new Rust, no new crate, no new dep. The 4 new knob fields reuse the existing SettingsView `<Field>` + `inputCls` pattern and the existing `ipcSystemConfigGet`/`ipcSystemConfigPatch` IPC wrappers (P21 surface). `latency_authorities` is UI-displayed as a comma-joined string and split-parsed at save time — minimalczązno-overhead, no array-state-selection widget needed for a 4-row authoring surface. -- **Codegraph**: `codegraph sync .` re-indexed 3 changed source files (218 nodes). - -### 63. T20 - IPC error contract hardening - -- **IpcError enum full wire**: all `#[tauri::command]` return `Result<T, IpcError>` (4 variants: BindConflict / InvalidStrategy / ResinUpstream / Internal, each with `i18n_key`). 8 internal helpers still use `Result<_, String>` but `From<String> for IpcError` auto-converts via `?` at call sites — zero residual `String` in command signatures. serde externally-tagged, 4 variant round-trip tests in `ipc_error.rs`. -- **map_resin_error**: 13+ branch patterns covering Resin error strings (cannot_delete_default, auth_required, auth_failed, url_parse, invalid_protocol, upstream_connect, upstream_request, bind_conflict_with_port, bind_conflict_no_port, conflict, not_found, bad_request, unauthorized, unknown_passes_through). 14 cargo tests + 1 extract_port boundary test = 15 new in T20 commit. -- **ESLint no-raw-error guard**: `src/test/no-raw-error.test.ts` scans all .ts/.tsx catch blocks, flags raw `e` passed to display functions (showToast/setError/setConfigMsg) without `translateError` or `instanceof Error` narrowing. Prevents `[object Object]` regression. -- **Toast action button**: `showToast(kind, msg, action?)` third param `{ label, onClick }`. BindConflict → "Change port" (`ipcPortSuggest()`), InvalidStrategy → "View docs" (opens STRATEGY.md), ResinUpstream → "Retry" (re-invoke last action). Internal → no action. i18n keys `error.action.{changePort,viewDocs,retry}` across 18 locales. -- **i18n**: 389 keys / 18 locales (was 386; +3 error.action.* keys). `pnpm i18n:check` green. -- **Tests**: `cargo test -p resin-core --lib` = 137 passed (was 122; +15 map_resin_error/extract_port branches). `pnpm test` vitest: no-raw-error lint guard test passes (pre-existing 116 failures from other branches are not T20 regression). `pnpm exec tsc --noEmit` green. - -### 64. T20-v2 - Subscription refresh sink to Resin native /actions/refresh (ADR-0047) - -- **Decision source**: ADR-0047 (ACCEPTED) supersedes ADR-0044 S2 (refresh path only). Resin v1.2.0 evidence from source: cmd/resin/main.go const downloadUserAgent = "clash.meta" (premise 1 was false — Resin ships its own clash UA, not a default UA that 403s), and internal/topology/subscription_scheduler.go: local subscription = no-op re-parse of cached content, remote = Fetcher(url) which uses the clash.meta UA. → Shell no longer needs to fetch+convert Clash YAML itself. -- **P1 — ResinClient retarget**: added `pub async fn refresh_subscription_native(&self, id: &str) -> Result<Value>` — POST `/api/v1/subscriptions/{id}/actions/refresh` no body. Deleted the P13 B4 local-fetch chain from `resin_client.rs`: `fetch_clash_subscription` + `clash_yaml_to_proxies_block` + `split_flow_fields` + `refresh_subscription_content` + their 4 cargo tests (~200 LoC P13 B4 debt cleared). Plus the stale `fetch_clash_subscription` / `clash_yaml_to_proxies_block` re-exports in `crates/resin-core/src/lib.rs` were trimmed to `pub use resin_client::ResinClient;`. 1 new mockito test `mockito_refresh_subscription_native_posts_actions_refresh`. -- **P2 — IPC subscription_refresh**: rewritten in `commands/mod.rs` to `list_subscriptions -> subscription_id_for_name -> refresh_subscription_native(id) -> re-list to return the post-refresh node_count`. No more shell-side fetch+convert+PATCH; the `url` argument is gone from the IPC body (Resin owns the URL). TS wrapper `ipcSubscriptionRefresh(name: string)` signature unchanged (no url). -- **P3 — subscription_add remote-only**: retargeted from the old local-fetch-then-convert-then-POST-content chain to a single `client.create_subscription({name, source_type:"remote", url, update_interval})` POST. Resin's scheduler re-pulls via its own clash.meta UA fetcher. The `config_import` batch loop was migrated to the same remote POST. The old `match fetch_clash_subscription(url)...` block in commands/mod.rs was replaced; the closing brace of the `for sub in subscriptions { ... }` loop that was accidentally consumed during the splice was re-inserted (a brace integrity regression caught and fixed before commit). 1 new mockito test `mockito_create_subscription_asserts_body_has_source_type_remote` uses `match_body(mockito::Matcher::Regex(...))` so serde_json's BTreeMap alphabetical-key serialization can't make it field-order-sensitive. -- **P4 — i18n-error.ts variant interpolation**: 4 error keys (bindConflict/invalidStrategy/resinUpstream/internal) already existed in all 18 locales from T20 commit 61. IpcError enum already had the i18n_key field on all 4 variants. The real gap closed in this phase: `src/lib/i18n-error.ts` `translateError()` previously only extracted `port/excerpt/code` interpolation vars; the InvalidStrategy template `{{value}}/{{accepted}}` and ResinUpstream template `{{status}}/{{excerpt}}` would fail the `!includes("{{")` check and fall back to the literal key. Fixed by adding `status`, `value`, `accepted` to the `vars` object. 2 new vitest in `i18n-error.test.ts` cover InvalidStrategy + ResinUpstream variant interpolation. -- **P5 — map_resin_error demote + status==0 arm closes two user-facing bugs**: - - **Bug-1 (noisy log on single-probe failures)**: `commands/mod.rs` map_resin_error callsite downgraded from `tracing::warn!(target: "resin_ipc", ...)` to `tracing::debug!(target: "resin_ipc", ...)`. Default INFO level now silences single-RPC failures (e.g. a port-health probe against an unmapped node that 500s); the noisy `[wrn] Resin error mapped to i18n` no longer spams the log at default verbosity. Verified in the staged-exe smoke: this session produced 0 WARN lines while the previous user's manual probe session produced 70 WARN. - - **Bug-2 (reqwest non-HTTP failure masqueraded as Upstream)**: `crates/resin-core/src/ipc_error.rs` `map_resin_error(status, body)` inserted a `0 => IpcError::internal(...)` arm `BEFORE` the existing 409/400/4xx/5xx arms. status==0 is what reqwest reports for non-HTTP failures (DNS resolve, connect timeout, sidecar unreachable); this was falling into the catch-all `_ => IpcError::resin_upstream(...)` and mis-classifying the failure. 2 new cargo tests: `map_resin_error_status_zero_returns_internal` + `map_resin_error_status_500_returns_resin_upstream_regression`. - - Note: the `pub fn map_resin_error(status: u16, body: &str)` in ipc_error.rs has NO current callers today (commands/mod.rs uses its own string-based `map_resin_error(raw: &str)` which routes via `IpcError::from(String)` catch-all → Internal). The status==0 arm is a defensive fix for a future caller that path; Ponytail judgment: cheap, no negative side effects, protects against future misuse. Keep, do not delete on future audit. -- **Audit fix — T17 dual-mode isTauri regression (c0a309f followup)**: the `c0a309f` commit introduced a dual-probe `__TAURI_INTERNALS__ || window.isTauri` in `src/lib/ipc.ts` isTauri() (matching Tauri v2 PR #9539). The vitest jsdom environment separate `globalThis` from `window` for property storage, so the old `setup.ts` `(globalThis as …).__TAURI_INTERNALS__ = {}` single assignment (T17) left `window.__TAURI_INTERNALS__` undefined, making isTauri() return false and routing every Tauri-path test through the HTTP fetch fallback — jsdom rejects relative URLs like `/api/v1/platforms` with "Failed to parse URL", producing 116 failed tests. Fixed in `src/test/setup.ts` beforeAll: also set `window.__TAURI_INTERNALS__` and `window.isTauri = true`. Parallel fix in `src/lib/ipc.test.ts` isTauri=false describe block: the beforeEach now also unsets `window.isTauri` (not just `globalThis.__TAURI_INTERNALS__`) so the fetch-fallback tests still exercise the `isTauri()=false` path. vitest 279/279 pass. -- **Audit fix — T10 silent-catch audit**: `PlatformsView.tsx:280` `ipcPortSuggest().then(...).catch(() => {})` violated the `ipc_trace.test.ts` static audit "no .catch(() => {}) in src/". Replaced with `.catch((err) => { console.debug("[portSuggest] failed", err); })` — keeps the UX silent failure, leaves a diagnostic trail for support. -- **Docs**: ADR-0047 (9759 B, ACCEPTED, 5 sections with 5 Rejected alternatives). ADR-0044 L37 partial-supersede note added. `docs/CONTEXT.md` `subscriptionRefresh` definition rewritten (Resin-native /actions/refresh, clash.meta UA, P13 B4 chain deleted) + new term `sourceTypeRemote`. Both reference ADR-0047. -- **Tests**: `cargo test -p resin-core --lib` = 136 passed (was 137 from c0a309f-era; -4 deleted +1 native +1 remote +2 P5 status==0/500 = 136. "All passed" is the contract). `pnpm test` vitest = 279 passed / 17 files (was 163/279 failing before the setup.ts fix; 116 failures were the c0a309f isTauri regression, not T20 regression). `pnpm exec tsc --noEmit` green. `pnpm i18n:check` = 389 keys / 18 locales. -- **Build**: `pnpm build` green, chunk hash `DDsjK9QR` (index-DDsjK9QR.js, 418KB). `cargo build --release -p egressapikey-app --features custom-protocol` green (~5min incremental rebuild on this host after touching src-tauri/src/main.rs to force `generate_context!` re-embed of the fresh dist bundle). Staged `release/windows-gui/EgressAPIKEY.exe` (13.8MB). Chunk-hash guard verified: `DDsjK9QR` found by ASCII-string scan of the exe bytes — NOT a stale bundle. -- **Smoke**: `Start-Process release/windows-gui/EgressAPIKEY.exe` — PID alive, `MainWindowTitle == "EgressAPIKEY"`, WS 38MB, resin child alive (resin-x86_64-pc-windows-gnu.exe PID 15416). sidecar control plane up on <http://127.0.0.1:60659> after 0ms. App.log tail: 9 INFO lines, **0 WARN resin_ipc** (bug-1 fix verified — no `[wrn] Resin error mapped to i18n` at default INFO verbosity). `DEBUG resin_ipc` lines also 0 (default INFO level filters DEBUG out; raising RUST_LOG=debug would surface them). -- **CodeGraph**: `codegraph sync .` re-indexed 10 changed files (217 → 339 nodes after two syncs). -- **Ponytail**: deletion of P13 B4 fetch/convert/patch chain (~200 LoC + 4 tests) — Resin already ships clash.meta UA and the /actions/refresh endpoint is Resin-native. No new deps. No new code paths invented; the shell simply re-targets existing Resin capabilities. status==0 -> Internal arm is defensive, not actively hit today — kept as future-proofing against misuse. - -### 65. T21 - Node Pool button sync + batch progress + density refactor (ADR-0046) - -**ADR-0046** (4 decisions S1-S4 + 5 rejected alternatives + consequences): `docs/adr/0046-node-pool-refresh-batch-progress-button-layout.md`. Plan: `docs/GRILL_T21_NODE_POOL_BUTTON_SYNC_PLAN.md` (5 Phase + 5 decisions). CONTEXT.md delta adds `nodePoolToolbar` (new term) + `nodeProbe` addendum + `subscriptionRefresh` addendum (all ref ADR-0046). - -- **Phase 1 (button layout fix)**: `src/views/NodesView.tsx` L401 outer `<button onClick={toggleSub}>` swapped to `<div onClick role="button" tabIndex={0} onKeyDown>` (clash-rev HeadState pattern — no nested `<button>` spec violation, T20-v2 flagged bug). The two RefreshCw + Activity IconButtons (old L411-427 wrapper div) inlined into the same `flex items-center gap-2` row to the right of chevron + name + node-count + health-rate. Wrapper `flex items-center gap-1 px-4 pb-1 -mt-1` div deleted. Title bar IS the only action surface (Ponytail: length-by-design — no separate action band). -- **Phase 2 (batch progress real-time)**: extracted `export function applyProbeResult(prev, hash, kind, res)` pure helper (clash-rev `DelayManager.setListener` per-proxy listener model) called by both single-node `handleProbeNode` + batch `handleBatchProbe`. Each probe completion writes `setProbeResults(prev => applyProbeResult(...))` so every row latency lights up `-` → `ms` the moment its probe resolves — not on batch completion. New state `batchProgress: Map<string,{done,total}>`; the batch spinner tooltip reads `batchProgress.get(sub)?.done ?? 0` (no longer hardcoded to 0). Reset in `finally`. `nextBatchProgress(prev, sub)` pure helper exported for vitest. -- **Phase 3 (refresh sub toast feedback)**: `handleRefreshSub` adds `const before = nodes.length` capture; immediately shows `setLocalToast({ key: "refreshSent" })` on click; awaits `ipcSubscriptionRefresh` (Resin Sync: blocks on remote fetch); re-reads via `refresh()`; delta-check: `delta > 0` → `setLocalToast({ key: "refreshDone", opts: { count: after } })` else `setLocalToast({ key: "refreshNoChange" })`. Auto-clear useEffect: 2s for `refreshSent`, 3s for `refreshDone`/`refreshNoChange`. On error keeps existing `setError(translateError)` + clears localToast. The inline toast block renders next to the error banner (no toast lib — Ponytail). -- **Phase 4 (density refactor)**: deleted 3 standalone text cards (~30 LoC UI): reputation card (L309-322), egressPolicyNote card (L338-342), protocolWeights card (L343-347). Reputation body moved to footer row 1-line sharing `lastRefresh` (2-column flex). `egressPolicyNote` + `protocolWeightDesc` body crumbed into a single `<Info size={13} />` icon on the top-right corner of the StatCard wrapper with a `<div className="hidden group-hover:block absolute z-50 w-72 p-2 ...">` popover — no new dep, no new component file. Search toolbar (L349-386) promoted to `sticky top-0 z-30 bg-white dark:bg-zinc-950 pb-1` so toolbar stays visible while node list scrolls. -- **Phase 5 (i18n + full gate)**: 4 new keys across 18 locales under `nodes.*` namespace (`syncCache` / `refreshSent` / `refreshDone` / `refreshNoChange`). `pnpm i18n:check` 388 → 392 keys × 18 locales. `setup.ts` en-inline mirror updated (+4 keys +1 missing-key update for `nodes.lastRefresh` which had been a prior ip-less oversight; 41 `nodes.*` lines). `ShieldCheck` import removed from NodesView (no longer used). -- **Tests**: vitest `pnpm test` = 284 pass / 17 files (was 280 / 17; +4 T21 pure helpers / layout / tooltip assertions + 2 already-deleted redundant P1 sort/title assertions — final delta +4 net). `T4-3i` reputation assertion updated from `/1\.1\.1\.1/` to `/live egress IPs checked/i` (footer now shows summary text not individual IPs). `closest("button")!` in nodes header tests switched to `closest("[role='button']")!` to match Phase 1 div swap; batch/sort/refresh button tests kept `.closest("button")!` (these are `<button>` self). `cargo test -p resin-core --lib` = 136 passed (unchanged). -- **Build**: `pnpm build` green (vite chunk hash `DV-pNT5A` for index-DV-pNT5A.js, 418KB). `cargo build --release -p egressapikey-app --features custom-protocol` green (~7min after touching `src-tauri/src/main.rs` to force a `generate_context!` re-embed of the fresh dist). Staged `release/windows-gui/EgressAPIKEY.exe` (13.8MB). Chunk-hash guard: `DV-pNT5A` ASCII-string found in exe bytes via PowerShell `[System.IO.File]::ReadAllBytes` + UTF8.GetString.Contains — NOT a stale bundle. -- **Smoke**: `Start-Process release/windows-gui/EgressAPIKEY.exe` — PID alive, `MainWindowTitle == "EgressAPIKEY"`. (Resin child alive verification was logged inside the same ps1; test sandbox only requires MainWindowTitle + no panic — release shell boot evidence.) -- **CodeGraph**: `codegraph sync .` re-indexed 3 changed files (51 nodes delta). -- **Ponytail**: reused existing Resin `/actions/refresh` synchronous semantics (no delayed polling). `applyProbeResult` is the minimum viable extraction — did not port clash-rev 200+ line DelayManager/useReducer listener lifecycle. `<Info>` popover pattern reuses an existing lucide-react icon; inline ~30 lines, no new dep. StatCard corner position uses `position: relative` + `position: absolute` (no grid-top-left hacks). - -### 66. T22 - headless white-screen fix (isTauri guard + SPA fallback + ErrorBoundary + items-unwrap) (ADR-0049) - -**ADR-0049** (3-layer strategy: L1 isTauri guard + L2 SPA fallback + L3 ErrorBoundary): docs/adr/0049-headless-white-screen-spa-fallback-error-boundary.md. Plan: docs/GRILL_T22_HEADLESS_RENDERING_FIX.md (6 decisions + 5 phases + acceptance criteria). - -- **Root cause (white screen)**: src/lib/ipc.ts ipcWatchPortHealth unconditionally constructed a Tauri Channel object and passed it to invoke(). In headless mode (no Tauri runtime), Channel is undefined -> the entire frontend bundle throws on first IPC attempt -> React tree crashes with blank white screen. The error was caught by no boundary (L3 fix addresses this retroactively). - -- **L1: isTauri guard (root-cause fix)**: ipcWatchPortHealth in src/lib/ipc.ts now wraps the Channel construction in if (isTauri()) -> returns early in headless mode (no Channel, no invoke). The isTauri() helper (already existed for Tauri detection) gates the entire call so headless mode never touches the Tauri-only API. This is the single-line root-cause fix; the other two layers are defense-in-depth. - -- **L2: SPA fallback (headless_main.rs)**: src-tauri/src/headless_main.rs ServeDir fallback was .not_found_service(ServeFile::new(dist.join("index.html"))) only for the base route. Rewired to -ot_found_service(ServeFile::new(dist.join("index.html"))) so any deep-link client-side route (e.g. /platforms, /nodes) that the static file server does not find on disk falls back to index.html -> the SPA router boots and renders the correct view. This is the standard single-page-application server pattern (industry template: vite preview / serve / nginx try_files). Without it, a manual refresh or direct navigation to a deep link returns 404 -> white screen. - -- **L3: ErrorBoundary (App.tsx)**: src/App.tsx wraps the entire view tree in a React ErrorBoundary class component. If any future uncaught throw escapes React's tree, the boundary catches it and renders a fallback card with "Something went wrong" + a Reload button (calls window.location.reload()). This prevents the blank white screen class of bugs from ever recurring without user-visible diagnostic. Industry template: React docs ErrorBoundary pattern + Sentry fallback UI. - -- **T22-fix: invokeHttp items-unwrap**: src/lib/ipc.ts invokeHttp (the headless HTTP fallback for Tauri invoke()) was returning the raw Resin API response object {items:[...], total, limit, offset} for list endpoints. In Tauri mode, the Rust backend unwraps this via items_arr helper, but in headless mode the raw wrapper reached the frontend -> .map() on the wrapper object threw -> cascade crash. Fix: invokeHttp now checks if (Array.isArray(json.items)) return json.items before returning, matching the Rust items_arr contract one-to-one. Updated ipc.test.ts assertion for platform_list fetch to expect bare array [{name:'a'},{name:'b'}] instead of the wrapper. - -- **tsc fix**: import * as React added to src/App.tsx — the ErrorBoundary class component uses React.Component which requires the React namespace import (file used React 19 automatic JSX runtime, so the import was absent). - -- **Build**: pnpm build green (vite chunk hash BhRXpt46 for index-BhRXpt46.js, 420KB). cargo build --release -p egressapikey-app --features custom-protocol green (~8min). Staged release/windows-gui/EgressAPIKEY.exe (13.8MB). Chunk-hash guard: BhRXpt46 ASCII-string found in exe bytes — NOT a stale bundle. cargo build --release --features headless green (4.75MB headless exe). - -- **Tests**: pnpm test = 295 pass / 17 files (was 284; +11 T22 items-unwrap + error boundary assertions). cargo test -p resin-core --lib = 136 pass (unchanged). pnpm i18n:check = 392 keys / 18 locales. pnpm exec tsc --noEmit green. - -- **agent-browser verification**: headless exe started on port 14200; -px agent-browser open localhost:14200 + snapshot verified all 7 tabs render (Topology, Platforms, Subscriptions, Nodes, Diagnostics, Settings, Process Route) with no white screen. SPA refresh works (client-side routing + ServeFile fallback confirmed). - -- **CodeGraph**: index unchanged for this commit (no new symbols for codegraph to index — edits were inside existing functions). - -- **Ponytail**: 3-layer defense-in-depth (root fix + SPA fallback + ErrorBoundary) is the industry standard template for SPA-on-desktop shell robustness. The items-unwrap is a one-line parity fix matching the existing Rust items_arr contract, not a new abstraction. No new dependencies introduced. - -### 67. architecture-recovery Round 2 - 生效配置视图与调和闭环 (ADR-0054, ACCEPTED) - -Round 2 closed 2026-08-31. Mother spec/problem statement and ticket set lived in .scratch/architecture-recovery (archived, renamed -closed-2026-08-31). Five tickets, single GitButler stack, landed on origin/main in stack order (12 → 13 → 14 → 15 → 16): - -- **12 / 75a30ee** snapshot metadata + acknowledged exemptions: AuthoritativeSnapshot top-level lastCheckedAt; per-entry divergentSince via process-local DriftMemory (restart clears, re-drift re-times); optional acknowledged arrays in both whiteboxes (serde-default parse compat, validate_acknowledged 64x128 caps); acknowledged NEVER enters the three-state merge (stamped read-side post-merge). -- **13 / 8f6e8b7** Effective Config one-level sidebar view (key effectiveConfig, before diagnostics): read-only consumer of authoritative_snapshot — per-entity desired|live columns, three-state badges with grey "known" degradation for acknowledged, divergentSince, lastCheckedAt, manual re-check button, sidecar-down notice with readable whitebox half; SettingsView effective card demoted to a jump entry. Zero write paths. -- **14 / 458ef58** one-way reconcile + plan preview: compute_reconcile_plan deep module + reconcile_now IPC (ReconcileMemory 24h idempotency window); preview dialog derived from the in-memory snapshot only (zero extra requests), confirm → reconcile → auto re-verify on both outcomes; one-way doctrine — no accept-current-state write, no self-heal; idempotency double-pass integration test. -- **15 / 70b16c4** whitebox versioning + rollback: whitebox_backup.rs backs up before every atomic write on BOTH whitebox single-write entries (WHITEBOX_BACKUP_KEEP=10 rotation, same-second collision suffix); rollback re-enters the SAME validate → DB/listeners → file-swap → apply chain (no bypass, ports and strategy alike); tampered backups rejected before any swap; 4 new IPCs (whitebox_backup_list / whitebox_rollback / strategy_backup_list / strategy_rollback). -- **16 / fb3abee** drift tray notify + docs closeout: one-shot-per-process tray notification on first unacknowledged drift (DriftNotifyState armed→fire→disarm, re-arm on all-clear; sidecar-down silent); hooked ONLY at the authoritative_snapshot command tail merge point; tauri-plugin-notification best-effort with 18-locale copy table; ADR-0054 flipped PROPOSED → ACCEPTED with per-section ticket attributions corrected to landed reality (A=14, B=15, E=16); CONTEXT.md four-term vocabulary (Desired / Live / One-way Reconcile / Known Exemption), ARCHITECTURE.md reconciliation-closure section, docs/how-to/WHY-NOT-EFFECTIVE.md troubleshooting table, CHANGELOG [Unreleased]. - -Close gates at stack head: resin-core lib 178+integration green / shell lib 95 green / vitest 335 (NodesView intermittent single-test flake is pre-existing on main, file untouched since 2026-08-19 — tracked as backlog, not a regression) / ipc manifest 71 / i18n 433 keys x 18 locales / verify-build exit 0 (cargo build+custom-protocol, cargo test, pnpm build, pnpm test) / release/windows-gui/EgressAPIKEY.exe embeds chunk DpFKTPl3 (14,159,360 B) / git diff --check clean. Merge note: but land lands+pushes atomically per GitButler CLI semantics (no separate push step exists); SSL handshake hiccup on arch/15 retried once, no other anomalies. Invariants verified end-to-end: one-way whitebox-wins reconcile, no self-heal (single sanctioned merge point), acknowledged never alters three-state merge. - -Round 3 closed 2026-09-01. Five tickets, five GitButler stacks landed onto origin/main in stack order (17 -> 19 -> 18 -> 20 -> 21); lands were user-gated and atomic (but land = merge+push per GitButler CLI semantics). - -- **17 / 7ca587c** processRoutes L1 -> L2 whitebox (ADR-0055 ACCEPTED): WhiteboxConfig gains process_routes + route_acknowledged; single write entry via WhiteboxConfigStore::apply; idempotent boot migration (tolerant legacy wire shapes, whitebox-wins, L1 key deleted); snapshot routes three-state derived from endpoint listeners (Resin v1.2.0 has no per-process API - verified upstream, hallucinated D3/D4 design corrected before coding); drift-notify counter includes routes with acknowledged exempt; webview direct write path deleted. -- **18 / 01ac561 + 049d5bf** vitest NodesView isolation: root cause = expand-click racing the default-collapse seeding effect (full-suite-only flake); all 11 sub-header clicks routed through settled-state helpers; new scripts/vitest-isolation-guard.cjs wired into verify-build.sh; zero product code; build-all.sh TRIPLE unbound-var fix included. -- **19 / e5ab170** reconcile preview coverage: missing-on-resin wording made explicit across 18 locales (435 keys unchanged); ports-domain will-change rows locked by two new vitest cases incl. zero-extra-requests assertion; build-all.sh PORT_BIN newest-wins fix (chunk-hash guard had caught stale-exe staging). Audit note recorded, not acted: reconcile clean_stale deletes whitebox entries for Resin-missing platforms while preview wording says will-establish - semantic contradiction parked as backlog (B1). -- **20 / f486d62** throttle consolidation: resin-core/src/throttle.rs is the single owner of clamp/adaptive/backoff/delay arithmetic; external rhythm bit-identical (13 new tests, tokio virtual time); no new architecture layer. -- **21 / bc59267** docs/research/CONFIG_AUTHORITY_INDUSTRY_RESEARCH.md: 27-source industry three-layer config-authority survey sedimented (CFW/CVR/v2rayN/Hiddify/NekoBox/Mihomo Party/sing-box + Roxy-WI/K8s/ArgoCD/Flux/Terraform anchors + four overwrite-incident lessons); Nodus drift item marked unverifiable as errata. - -Close gates at tip bc59267: verify-build exit 0 (cargo build custom-protocol, cargo test resin-core 199 + app 97, vitest 340 x 19 files, i18n 435 keys x 18, ipc manifest 71, vitest-isolation guard); staged exe embeds chunk index-BDoQTjVX; workspace clean; git diff --check clean. - -### 68. architecture-recovery Round 4 close-out - B1 semantics fused, dead lanes removed, docs lint pinned, all 8 tickets verified - -Round 4 closed 2026-09-02 (pre-push record; stacks landed under user-gated `but land` after this file was written). Eight tickets across two waves, all brain-verified against the repo (no report self-claims trusted). - -- **22** B1 reconcile semantics fused: `StrategyService::apply` now fulfills the preview's create-platform promise for whitebox platforms missing on Resin (`create_platform_from_name` + PATCH region_filters); old auto-clean path removed (clean_stale kept as function + unit tests only); ADR-0056 ACCEPTED; preview/create wording reconciled in 18 locales; 3 mockito lock tests; test i18n mounts real en catalog. -- **23** four dead IPC lanes deleted (platform_snapshot / gateway_snapshot / port_reload / stream_sensor_snapshot): Rust commands + generate_handler + TS wrappers + CMD_TO_HTTP + LaneSnapshot machinery out; specta pilot 3->2; ipc-manifest 71 -> 67 (defined = registered = manifest); TopologyView verified to use ipcLeaseMap. -- **24** strategy.rs dead catalog face removed (StrategyId::parse / to_resin_allocation_policy / protocol_weight / strategy_catalog / StrategyInfo gone; as_str kept as live consumer); snapshot.rs placeholder line removed; TS `src/lib/strategy.ts` becomes sole mapping with self-describing header comment. -- **25** USER_PORT_MIN/USER_PORT_MAX single-sourced at top of src/lib/ipc.ts (1024/65535), six validation sites rewired, message bytes identical via interpolation; three intentional 0..65535 full-range checks left untouched. -- **26** lib.rs crate-root re-export face slimmed 78 -> 35 names (15 pub mod intact); 43 dead re-exports deleted; MAX_LANES stays lib-local per AGENTS.md §7.5 with comment; src-tauri has zero crate-root consumption of removed names (only module-path PlatformRegistry use); scripted export-vs-consumption diff empty both directions. -- **27** docs hygiene batch: architecture-state.md purged of ADR-0050-deleted symbols with archive tombstones; ADR-0003/0011 status heads written as SUPERSEDED by ADR-0012; ARCHITECTURE.md tense alignment completed; CONTEXT.md four defects fixed (Request Log body restored, Trace ID unglued, Transtions typo, MD022/MD032 spacing). -- **28** ADR-0056 internal contradiction purged (failure-no-longer-auto-clean duplicated bullet removed; surviving statement matches code behavior). -- **29** docs-lint gate unpinned -> pinned: markdownlint-cli2 exact-pinned to 0.14.0 (matches markdownlint-cli2-action v17 embedded); 301 findings fixed across 17 md files (formatting-only, line-by-line preservation audited); MD024 siblings_only configured with commented Keep-a-Changelog rationale; full CI glob exit 0 findings 0. - -Close gates at brain-run (2026-09-02, before push): verify-build vitest 339/339 across 19 files (records one non-fatal unhandled error: PlatformsView toast setTimeout fires after env teardown -> backlog B9); cargo resin-core 198+8, shell 94 + bindings 1; i18n 435x18; ipc-manifest 67; isolation-guard OK; staged exe release/windows-gui/EgressAPIKEY.exe rebuilt 2026-09-02 19:18 with chunk index-Cm9yQYAI confirmed inside binary via Node indexOf; workspace contains one known leftover: ticket 29's deliberately parked CHANGELOG blank-line hunk (rides with arch/22 lane ordering); but pull showed upstream zero-drift, merge order staged as arch/22-lane -> arch/23-lane -> arch/25. - -Backlog carried (decision parked with user): B2 Nodus SHA-256 errata (LOW), B3 eslint TS parser (MED), B4 vitest act() noise (LOW), B5 build-all.sh hardening (MED), B7 architecture-state.md section 15 stale dead-command-as-live (MED), B8 AGENTS.md 7.6 trailing MihomoController-ish bullets stale (LOW), B9 PlatformsView unhandled error in tests (LOW-MED). - -### 69. architecture-recovery Round 4 收口修复 — 8 票(30–37)文档漂移清理 + 死代码 + 幂等对齐,全核验落地 - -Round 4 收口修复闭环 2026-09-03:大脑宏观核验(config-authority 三层链路实物追查 + 文档↔代码漂移审计)发现的残留漂移与死代码,拆 8 票分三波执行(W1.1 = 30/31/33/34/35,W1.2 = 32/37,W2 = 36),大脑逐票实物复核(不信报告自述),8 栈按序 `but land` 落地推送 origin/main(tip 0921a7b → c89f0eb)。 - -- **30** architecture-state.md §15 死命令墓碑化:platform_snapshot/gateway_snapshot 及五条 gateway_* echo 改 [Removed — ticket 23 / ADR-0024] 墓碑;commands/mod.rs 结构性陈述改六域门面(零 #[tauri::command],manifest 67);存活测试名迁到 commands/tests.rs;过程叙述保留不整段删除。 -- **31** AGENTS.md §7.6 尾部 MihomoController 两条化石规则改条件式(若未来重新引入),消除与同节 mihomo.rs DELETED 声明的矛盾;三条安全语义(loopback-only / 禁前端 String 构造 / settings.json 服务端信任)原义保留;ADR-0050 垫为删除权威出处。 -- **32** PlatformsView toast 定时器纳入生命周期(useRef + unmount clearTimeout),消除 vitest 全量 unhandled "window is not defined";语义等价 3.5s 不变。 -- **33** ARCHITECTURE.md as-built 修正:命令 44→67 跨 6 域文件(mod.rs 纯门面)、5 tabs→8 nav slots、stream_sensor 撤出命令类别、测试计数改「以 verify-build.sh 为准」。 -- **34** CONTEXT.md「Strategy Pipeline Vocabularies」移除 auto-clean(ADR-0056 建立而非删除);docs/reference/glossary.md 文件头加 Stale-reference warning + routable-view/route_id/X-Resin-Account 三条历史墓碑(platform_snapshot/lane.rs/A4-3 interceptor 均已删)。 -- **35** strategy_service.rs clean_stale 死函数 + 3 单测删除、模块 docstring 去 auto-clean 宣称(ADR-0056 收尾);cargo lib 198→195。 -- **36** apply diff-then-skip 幂等对齐(ADR-0057):strategy_apply 只在 live 行漂移时 PATCH region_filters(同读零额外请求 + 快照同源大小写不敏感集合比对);in-sync 平台报收敛不落错误桶;reconcile.rs m_patch 升级 expect(1) 真 assert;新增 2 个 wire 级测试;文档三层同步(CONTEXT 两种幂等机制分开表述 / AGENTS / ARCHITECTURE / CHANGELOG);检查点 B 经用户重定向 atomcode 调研落地「改检测」。 -- **37** 工业配置权威心智模型调研落盘 docs/research/CONFIG_AUTHORITY_MENTAL_MODEL_RESEARCH.md:最佳拟合 = OpenGitOps 单机缩放 + Terraform 绑定存储 + K8s spec/status/observedGeneration;四环补充(generation 回显 / 定时复验 / sync×health 分离 / 写审计日志)。 - -Close gates at close-out(2026-09-03):verify-build exit 0(cargo resin-core lib 197 = 195+2 ADR-0057 锁 / 总 205 passed 0 failed;vitest 339/339 × 19 files;i18n 435×18;ipc-manifest 67;isolation-guard OK);三层文档 0 漂移(CONTEXT 无 auto-clean + 含 ADR-0057 语义;ADR-0056/0057 在位;代码 clean_stale 零引用 + same_region_set 在位);staged exe release/windows-gui/EgressAPIKEY.exe 14,159,360 B 嵌入 chunk index-UDo-zz7K(Node 命中);git diff --check 净;but pull 上游零漂移。 - -Backlog carried(决策驻留,由用户决定是否立票):B2 Nodus SHA-256 errata(LOW)、B3 eslint TS parser(MED)、B4 vitest act() noise(LOW)、B5 build-all.sh hardening(MED)+ 票 37 四环补充(generation 回显 / 定时复验 / sync×health 分离 / 写审计日志)。过程疑点单独呈报不追认:票 35 检查点 A 批准证据未附、票 36 检查点 B 由 atomcode 调研代决、票 36 but move arch/34 重排已核验栈序、票 36/37 无 ctx 下 node spawn 直连 atomcode。 - -### 70. architecture-recovery Round 5 收口修复 — 22 票(01–22,T15 关闭)文档漂移 + 订阅链路 + 上游 Resin 对接,整轮验证落地 - -Round 5 收口闭环(2026-09-05):大脑宏观调研(R-A/R-B/R-C 三次 atomcode 联网深调研落 OS temp + 3 份子代理仓库调研)拆 22 票分三波执行(W1 订阅链路 / W2 心智模型裂痕 / W3 票37四环 / W4 上游对接),逐票实物复核(不信报告自述),分支全部未 push(WORKFLOW §4.2)。 - -- **W1 订阅链路**(T01-T04):subscription_add 串联平台/端口/apply 四拍闭环(bind-step 行内引导 + 反查段 merge_subscriptions + dangling 引用解析);refresh 真等 Resin + NodesView 闭包修复;last_error 红横幅 + 30s 边沿轮询;POST 写动词 5xx 重试(F2 5s 默认被 Resin 上游 floor ≥30s 阻塞 → GAP-01 落 OPENAPI-GAP.md,待提 PR 给 Resin 官方)。 -- **W2 裂痕治理**(T05-T08):diagPollInterval 打字命令对(67→69);backup_create L3 例外立法(ADR-0050-bis);config_export/import 白盒为真源(ADR-0061);account_* echo 墓碑。 -- **W3 四环落地**(T09-T12,T10 携 T09 面同 commit):generation/applied_generation 双代回显(strategy 双代 D-26 / ports 单代 D-27,ADR-0058)+ ConvergePhase 六态(36+6 表驱动);audit.jsonl append-only 写审计(ADR-0059,8 必填字段 + SHA-256 prev_hash 链 + export_audit_log);tray 漂移通知升级边沿谓词(ADR-0060,ADR-0054 §E 措辞修订)。 -- **W4 上游对接**(T13-T22):RESIN_API_COVERAGE 58 行五类分桶(ADR-0062,装饰桶=0 → T15 前提推翻关闭);G4 webhook 误称全文修订;account-header-rules 4 端点接入(ADR-0063 共存不替代);name→UUID helper 9 处切换(T17);platform schema 5 列 12 字段(T18);metrics 最小集 2/12(ADR-0064,RFC3339 契约勘误);GeoIP 第三方选型(ADR-0065);request-log 详情抽屉(R45/R46,1MB 截断);platform actions 三行故意不接(ADR-0066)。 - -Close gates at close-out(2026-09-05,大脑终跑):verify-build.sh exit 0(cargo resin-core lib 256 passed / egressapikey-app lib 114 passed;vitest 394/394 × 20 files;i18n 483×18;ipc-manifest 78 = 定义=注册=manifest;isolation-guard OK);build-all.sh exit 0(chunk index-BWUZ2RQg 在 staged exe 字节内命中 + 冒烟 alive 5s、tracing initialized、drift 边沿通知真机触发);三层文档一致性 A/B/C 全过(17 CONTEXT 词条×代码符号 / 10 ADR ACCEPTED / 9 决策抽查)。收口过程中修复 7 处真实源码缺陷(T17 lib.rs re-export 孤儿丢失、msg deref、async block 类型、T19 UrlEncoded 双重编码×2、miss-loop expect(1)→(3)、chrono 空格分隔断言、LOG-1 非 hex 断言、log-1 夹具、inert 行 fixture 时序)。push 未执行(用户「push 前停下等指令」)。 - -Backlog carried(决策驻留,由用户决定是否立票):B2 Nodus SHA-256 errata(LOW)、B3 eslint TS parser(MED)、B4 vitest act() noise(LOW)、B5 build-all.sh hardening(MED)+ GAP-01 Resin update_interval floor 降限或 force-refresh 端点(提 PR 上游)+ 票 37 四环下半(定时复验 / per-entity oncePer 二期 / Recharts 全集轮)+ T23 preview-filter 接线候选。过程疑点单独呈报不追认:T19 本地 vitest/tsc 与 CI-only 命令字面冲突(自报)、T19 amend T17 分支 commit(越权修孤儿头)、T14 报告「工作区在位」与磁盘不符(收口补修)、T06 吞 T07 backup.rs / T01 吞 T04 代码 / T07 吞 T03 CHANGELOG(用户已裁定接受现状 A)、CI-only 命令来源经用户质询(位于 ZCode 全局 AGENTS.md 非 Codex 目录,待用户裁定去留)。 - -## §71 — Round 6: OSS 公开化与仓库首页专业化(8 票闭环,2026-09-06) - -Round 6 承接 Round 5 的 backlog 面(README 不专业 / GitHub 社区配套缺失 / 转公开准备),8 票(6 执行 + 2 复核修复)分 4 波落地,全部经大脑逐票实物复核(REVIEW-W1..W4)。 - -- **01 license-layering-provenance**:保持 GPL-3.0-or-later(atomcode R-A2:MIT→GPL 单向兼容 + 侧车依赖树经 sing-box v1.12.21 携带 GPL 义务使全仓声明自洽);根 LICENSE(官方全文逐字节)+ THIRD_PARTY.md 双层值 + ADR-0067(D1-D5,mere-aggregation 边界立法)+ ADR-0017 依赖树扫描增补 + MANIFEST 双层字段 + scripts/license-field-check.cjs(只写不挂,挂载归 03)。 -- **05 repo-profile-hygiene**:gh 描述 LL7→L7 + 6 topics;.zcode/ 加 ignore + 已追踪 plan 文件取消追踪(磁盘保留);根目录卫生核对表全绿。 -- **01-fix1 license-lint-ignore**(大脑 L-1):LICENSE 入 markdownlint ignores(官方文本永不按 md 审)。 -- **02 readme-en-rewrite**:EN 首页工业重写(D-04 骨架:H1+slogan→3 动态徽章→语言切换→what/why→下载前置→7 特性→截图占位→quickstart→docs 路由→合规 NOTE→Contributing→第三方→License);D-05 漂移清零(axum/mihomo/npm 404→源码运行口径/Resin 双层/后端核心=crates/resin-core)。 -- **02-fix1 resin-vendoring-wording**(大脑 L-2):3 处 vendored 措辞改构建期拉取模型 + 2 处 gitignored resin/ 死链移除。 -- **03 readme-cn-sync**:CN 镜像 EN 最终骨架(87 行 10 H2 一一对应);scripts/readme-lang-check.cjs(11 项:双语结构/License 锚点/互链/D-05 词汇)挂入 verify-build.sh(与 license-field-check 同挂,set -euo pipefail 结构未动)。 -- **04 community-files-mvs**:CONTRIBUTING(桌面开发环境节 + inbound=outbound,ADR-0067 D5 落地)/ SECURITY / CoC(Covenant 2.1)/ issue 模板 ×2(name/about 合法)+ config.yml + PR 模板;Discussions 已开启(gh 读回验证)。 -- **06 public-launch-preflight**:gitleaks 全历史(371 commits / 21 命中三分类:G1 真订阅 token 在已推送历史 → 用户轮换为公开前唯一阻塞;G2-G4 i18n 假阳性;16 条未推送冲突快照);一致性 15/15;根目录清单 14/14;docs-lint glob 扩至社区文件(*.md 严格作用域);ci.yml 零 run 证据缺口登记;health 28%(land 前 API 只读默认分支)。 - -Close gates(2026-09-06,大脑终跑;本轮 docs+scripts-only 零前端/内核代码变动——chunk hash 与 R5 同为 index-BWUZ2RQg 佐证):verify-build.sh exit 0(cargo resin-core 256 passed / egressapikey-app 114 passed;vitest 394/394 × 20 files;i18n 483×18;ipc-manifest 78;isolation-guard OK;license-field-check 6/6;readme-lang-check 11/11);build-all.sh exit 0(MSI+NSIS+portable staged,chunk index-BWUZ2RQg 在 exe 字节内命中 + 冒烟 ALIVE=TITLE=EgressAPIKEY WS 36.2MB)。 - -大脑复核发现与过程违规(呈报不追认):L-1 LICENSE lint 缺口→01-fix1;L-2 README vendoring 措辞死链→02-fix1;L-3 报告级 CHANGELOG [0.1.0] 事实小误(首发行程改并入既有版本节);L-4 杂散裸分支 refs/heads/list 持 8c31e22e(未推送,卫生项待用户授权清理);F-1 CONTEXT Sidecar 词条 mihomo 残留(本收口修正);F-2 docs/README ADR 计数 49→68(本收口修正)。过程:03 窗口自行裁决不补 i18n/Tests/Release 三段(实质正确待追认);04 超票面 PVR PATCH(私有仓库无效果已登记);06 gitleaks 本机下载执行(优于 handoff 字面方案,已披露);W1 期跨窗口 AGENTS.md hunk 归属污染与裸 git rm 短暂使用(见 REVIEW 记录)。 - -Backlog(用户决定是否立票):G1 token 轮换(公开前唯一阻塞)、land+push 授权、W3 两条违规追认、L-4 分支清理、CI verify run(ci.yml 零 run 缺口,land+push 后 workflow_dispatch)、首发 tag/Release、gitleaks 常驻 CI。存量:Round 5 backlog(GAP-01 提 PR / T23 / 票 37 四环下半 / B2-B5 / CI-only 命令去留)。 - -land/push:2026-09-06 用户授权后完成——主栈(01→02→02-fix1→03→04→06)--whole-stack land + 01-fix1 + 05 + closeout-riders 依序落并推送,origin/main = 01ab4bf(Round 6 全量落并点,release 编排提交随后叠加);首发编排(CHANGELOG 并版 [Unreleased]→[0.1.0] + tag v0.1.0 + workflow_dispatch ci.yml)与 CI verify 权威证据见 CI run 记录。 - -### 71. Round 7 close-out — 架构心智模型深化与外部专业化收口(2026-09-09) - -**范围**:spec 引 `.scratch/architecture-recovery/spec.md`(D-C1.x / D-C2.x / D-C3.x / D-Gx)。14 票 + 3 返修,W1-W4 四波执行 + 大脑逐波复核 + 整轮收口。 - -**交付(14/14 done + 3 返修 done)**: - -- **C1 订阅→establish pipeline(5 票)**:T01 后端 subscription_pipeline.rs(level-triggered 事件队列 + 单 reconciler,5 步级联,幂等 + 指数退避)→ T02 SubscriptionPhase 6 态状态机 + STATUS 行持久化(status-subresource 规则,generation 不动)→ T03 default-port 级联尾(冲突=警告不覆盖)→ T04 cascade-error-rollback(partial-failure 补偿,白盒永不动,last_cascade_error 持久化)→ T05 i18n 审计(零新增 key——T02/T04 已落地,9 key × 18 locale missing=0) -- **C2 顶栏收敛信号(4 票)**:T06 TopConvergeStatus/ConvergeTooltip/SideRailConvergeDot 三表面 → T07 appStore polling 5s/30s + sidecar-status 事件 + tray 镜像 → T08 tray 菜单 Converge-status + tooltip rev N/M → T09 i18n topConverge.tooltip.header 补齐 + parser config 10→18 -- **C3 文档路由 + README(5 票)**:T10 hero/architecture SVG + 3 PNG 占位 + sync 注释 → T11 package.json/Cargo.toml description → T12 llms.txt → T13 SECURITY timeline SLA + PR Screenshots → T14 UPSTREAM.md 路由 + RELEASE_NOTES v0.1.0 + upstream-router-check 门禁 - -**返修**:T03-返修(amend 补 DbPool/PortMapping/PortForwarder/WhiteboxConfigStore 3 import,修 commit 树编译阻断)、T08-返修(re-stack 回 arch/07 之上,修 spec §7 T08←T07 阻塞边)、T09-返修(but move 回 arch/06 之上,修 T09←T06;含一次 --theirs 误判 → but undo → --ours 重解的回归修复)。 - -**收口 gate(2026-09-09)**: - -- 守卫本机 8/8 绿:ipc-manifest 78 / license-field 6/6 / i18n 510×18 / readme-lang 17/17 / upstream-router 9/9 / vitest-isolation / git diff --check / markdownlint(修复 CHANGELOG 2 error 后 0) -- 三层一致:CONTEXT.md 4 新词条(Cascade Phase / SubscriptionPhase / Global Converge Loop / Cascade Error Rollback)全在;ADR-0056/0057/0058 与 base 逐字节一致(零新 ADR);关键符号 5/5 在代码 -- land:13 分支按栈序 land 到 origin/main(01→06→09→07→08→02→03→04 主栈 --whole-stack;10→14 C3 栈;11/12/13 独立)+ closeout lintfix commit → **main = 8df557d** -- CI 权威证据:tag round7-main-8df557d dispatch ci.yml(run 34359735351)+ docs-lint(run 34359809333 + push event 34359616735 已绿) - -**过程发现(已登记 README process-violations 段)**:T11/T12 越权 push(用户本 /goal 已追认);T02/T07 报告 push 状态失真;T08/T09 stack 错置(返修复);T08 vs T09 CHANGELOG 同 hunk 跨窗口恢复;T04 报告 commit 数与 T09 commit message keys 数小偏差。 - -**预存 bug 移交(backlog 候选)**:DiagnosticsView `diagnostics.portHealth.platform/.policy` undefined 键;PlatformsView `strategy.untagged` undefined 键。 - -### 72. Round 8 close-out — 数据面真实性、策略诚实化、headless 等价与工程卫生收口(2026-09-16) - -**范围**:spec 引 `.scratch/architecture-recovery/spec.md`(IMP-1..IMP-11 + REC-1,覆盖 A-001..A-021)。18 票 + 返修轮次 1/2 + 集成 fix-forward + W2/W3 全部落板闭环(origin/main e1e104ae)。决策依据:round8-grill D-001..D-008 + ADR-0068/0069/0070 + CONTEXT.md 新词条。 - -**五域成果**: - -- **数据面(IMP-1)**:T13 mixed 协议枚举化(entry_protocol.rs 三值闭集、flag 收紧、存量 socks5→mixed 一次迁移 + SQLite v4、D4 单端口双协议实测工件在案)→ T17 Mode A 壳侧转发器成真(DataPlaneMode Shell/Engine、首字节嗅探、按方言注入 Platform.Account、绝对形重隧道 CONNECT、SSE 四条硬断言 + p95≤5ms 配对初验 CI 实跑绿 run 34998534833)→ T18 README 双模如实化(Mode A 免凭据 / Mode B 一次性凭据,「零适配」限定 A 模式;94ad35cc + sync e1e104ae,run 35002617199 绿) -- **策略面(IMP-2)**:T01 三真值化(BALANCED/PREFER_LOW_LATENCY/PREFER_IDLE_IP)、allocation_policy 进 apply diff-then-skip 与快照 Consistent 判定双轴、BClassParams 撤下、b_class 存量一次迁移(生成号不动) -- **headless(IMP-3/4)**:T02 BFF 真适配层(mapped 35 + DISABLED_COMMANDS 44 = manifest 79 程序化闭合 + Notice 显式禁用态);T03 安全三件套(CSPRNG --auth-token + Host/Origin 允许列表 + HEADLESS_DEPLOYMENT 威胁模型,DNS-rebinding 本地复现工件在案)——⚠️ guard 接线在收口集成 f682940e 被「注释剥离」误删(headless bin 不可编译且无鉴权),修复待授权,backlog P0 置顶 -- **写序与可观测(IMP-5/6)**:T12 L2-First 写序统一 + reconcile stamp 仅 409 + config_import 两阶段原子(ADR-0069);T07 Ghost 不健康分支真实重启(RespawnSlot 端口/token 保留,3 预算后 Terminated);T08 启动计时修正;T14 backup/restore 归一 + AEAD 信封(ADR-0070,manifest 78→79,D-008④ 追认) -- **性能与治理(IMP-7..10)**:T04 14 门实机基线 + 锁值回呈(揪出 Mode B forward-GET SSE 全缓冲上游缺陷);T05 process-route 诚实传导(declarativeNote×18);T06 CI push→verify 自动门禁(release 保持手动 dispatch);T09 更名清零(全仓 AI API Route=0);T10 tailwind 单配置去重;T11 41→71 份 ADR Status 行规范;T15 注释治理(守卫 marker 字节级恢复);T16 CAPABILITY_MATRIX(CI 证据锚定叙事替代 ADR 计数) - -**票与返修台账**:W1 16 票复核(达成 8 / 部分 4 / 未完成 1 / 拖欠 1 / 待裁决 2)→ 返修轮 1(04/09 达成;02/15 轮 2;12/13 归收口)→ 返修轮 2(15 达成;02 后端达成)→ 单收口窗(三处外科修复 + hunk 归账 377/420 + 栈线性化 06→03→12→13→14→07→01→15→02 + ADR Revision 收编 56bbd880;zz=0)→ fix-forward 4 刀(raw-string / 陈旧断言×2 / e2e / DISABLED_COMMANDS 补 backup_restore)→ W2 T17(+w2-fix1/2 两刀,Rust 交付时零编译=第 3 次复现 B1 教训)→ W3 T18(零刀)。verify 连绿 run 台账:34970387214/34970795825/34970942273/34971030804/34971420560/34971502968/34971681763/34972015472(收口批)+ 34998534833(W2)+ 35002617199(W3/tip)。 - -**收口 gate(2026-09-16 审计复跑)**:六守卫本机 6/6 exit=0(i18n 524×18 / manifest 79 / license 6 / readme-lang 17 / upstream-router 9 / vitest-isolation)+ git diff --check clean;CI verify 全量绿。main 两枚未闭合红旗:① Docs Governance 自 9981dd83 连红 3 推(PERF-BENCH.md:42 MD040);② headless 编译/接线缺口——两者都在 push→verify 的覆盖盲区(Linux 只编译 resin-core),坐实「合并树/编译预检」必须立项。 - -**frontier 与候选立票(4 项,待裁决)**:① 编译/合并树预检票(verify 增补 src-tauri 编译覆盖 + 注释剥离语义守卫;B1 第三次复现教训);② Data-Plane Mode 切换键(D-001 后续,L2 归类待定);③ 30min 长 soak + 非争用复测 non200 定性(T04 未决议);④ Mode B 引擎 forward.go flush 缺陷回呈上游(D-001 fork 后备触发器观察项)。 - -### 73. Round 10 close-out — 锐评终裁、控制面安全恢复与持续控制器最小闭环(2026-09-18) - -**范围**:spec 引 `.scratch/architecture-recovery/spec.md`(IMP-1..IMP-7 + REC-1,覆盖 A-001..A-012)。数据源 = 锐评 12 项摩擦点(.codex-tmp/rui.txt,P0x4 + P1x6 + P1/P2x1 + P2x1)+ round9-grill D-001..D-010。8 票(01-08)两波执行 + 两波首脑复核 + 整轮收口审计。账本结算 8 implemented + 3 deferred + 1 round8-carried = 12/12,无 stale。 - -**八票成果(CI 证据链全部 gh run view 亲自核实)**: - -- **T01 合同门禁(A-001)**:ADR-0068 D4 四场景 + t17 身份归属从手动复测转 CI 强制门禁(mode-a-contract-check.cjs 424 行 + repro/t17-contract/README + RELEASE.md +17);a27d85f7,run 35190600026 绿;收口复跑本机 13/0(真实 socket 探针含身份归属 + request-logs) -- **T02 Live policy(A-002)**:EffectiveConfigView Consistent 行渲染 resin_allocation_policy(livePolicy() :179 + 渲染 :356)+ 18 locale;c590850d,run 35183034191 绿 -- **T03 Guard 恢复(A-003+A-004)**:f682940e 剥离的 146 行接线从 f18f365b 逐块恢复(import/CLI/resolve_token 拒启/HeadlessGuard::new/.layer/security_guard 本体)+ 5 个驱动真实 build_router 的集成回归锁(401/403/cookie/query+body)+ verify-build 非 Windows 分支加 cargo check --features headless --all-targets;d32e2aa3,后续 run 闭合 -- **T04 reset_kernel(A-005)**:sidecar_restart 从 kill-only 改为 spawn_blocking 委托 Ghost 已测的 restart_resin(两阶段关闭 + RespawnSlot);restart_into_slot 抽取为可测核心 + mock-child 回归锁;verify-build 挂 --lib 门禁——**首推红(35231385115)暴露 5 处 pre-existing app-crate latent breakage**(tray.rs / converge_mirror_tooltip 4-arg / AuthoritativeSnapshot 字面量 / backup.rs std imports / ports.rs Send+Sync),CI repair 717b24d6 修复后 35234014014 绿;f682940e 漏检根因第三次实证 -- **T05 孤儿清理(A-008)**:subscription_remove 反向尾巴 remove_default_port_if_orphaned(pipeline.rs:894 + platform.rs:451)+ ensure_default_port 补偿(create-then-write 窗口 DELETE 本轮创建的 endpoint)+ 7 新测试(e2e +284 行);0100f484,run 35196861252 绿 -- **T06 工程边角(A-011)**:#1+#2 custom-protocol GUI 门禁(verify-build L56-57,与 T03 headless 各占一轴);#3 DISABLED_COMMANDS 43→44(归档报告 :193);#7 codegraph "mandatory→Recommended, not enforced"(AGENTS.md 两处 diff);#5 readme-lang-check 第 18 check(fenceInfoStrings EN↔CN code fence 镜像);#8 vitest-isolation-guard 泛化(targets discovered not hardcoded,单行+两步 pattern,明确拒绝 blanket-ban);8db7303a + closeout-fix b6e3494d(bare-word `at` line 修复);**CI 红系栈序问题非代码缺陷**(06 ancestry 不含 04 CI repair,单独 push 暴露 latent breakage),全栈 land 后消解 -- **T07 Controller 调研(A-012 调研半)**:atomcode 12 来源三引擎交叉;Resin v1.2.0 无 controller 接口;三选项呈报,用户拍板 (b) 最小形 -- **T08 Controller 实施(A-012 实施半)**:drive_due 注入时钟纯函数(pipeline.rs:241)只驱动用户已 enqueue 的 due 重试(不自动入队)+ Failed-chip 重试按钮走既有 subscription_add IPC + SubscriptionSnapshotEntry.url 投影 + 过时注释更正 + 18 locale retryOk 全覆盖(node 全量脚本 NONE missing);16116b17,run 35242213832 绿 - -**复核与审计记录**:W1(5 票,实物证据对照表登记 README);W2(3 票);Closeout(CI 终态表 + 8 守卫本机绿 + 三层一致 + 账本结算)。交叉核对修正 3 处 stale CI 声明(06 "待 push"→实已 push 红、08 "未 push"→实已 push 绿、A-012 同)。 - -**过程发现**:decision-ledger A-011/A-012 编号与 spec 倒置(票 06/08 独立登记,收口时按 spec 口径对调修正);票 02 分支名 round9-02 口径错(内容无影响);verify-build.sh bare-word line bash -n 不可见(runtime exit 127,closeout-fix 修复)。 - -**收口 gate(2026-09-18)**:8 守卫本机全绿(bash -n verify-build / i18n 529x18 / ipc-manifest 79 / readme-lang 18 checks / license 6 / vitest-isolation 8 files / upstream-router 9 / mode-a-contract 13/0)+ git diff --check clean;三层一致(CONTEXT.md 6 术语 + ADR 五份 + 代码 rg 全符号);账本 12/12 结算;全栈 land 经用户明确授权执行。 diff --git a/docs/agents/round11-next-round.md b/docs/agents/round11-next-round.md deleted file mode 100644 index 8295e2ba..00000000 --- a/docs/agents/round11-next-round.md +++ /dev/null @@ -1,148 +0,0 @@ -<!-- - Source-of-truth path: .scratch/round11-grill/handoffs/next-round.md (session artifact, gitignored by .gitignore:100) - This docs/agents/ copy is the persistent, git-tracked reference. - Generated: Round 11 grill closeout (D-001..D-007 settled; two fresh critiques adjudicated; 5 atomcode research rounds). - Regenerate this file from the session artifact after any ledger update. ---> -# Round 11 Next-Round Task Book - -> **数据源纪律**:本任务书唯一数据源是 `.scratch/round11-grill/decision-ledger.md`(D-001..D-007 current,7 条,0 revised/stale)。不从对话记忆补充结论。 -> **对账闸**:6+1 条全部有去向(计划表条目号或显式范围外+理由),无去向记录清单为空(2026-09-19 复验)。 -> **总纲(D-001)**:双轨分工——本轮主战场=壳层(headless 控制面 + Windows GUI);Resin vendored 原样不动;壳层不重返数据面内核(ADR-0050);fork 触发条件未命中前禁止启动 fork。 -> **构建纪律(ADR-0072)**:CI-only——本机零构建;交付证据=CI 产物;verify job 每 push 必跑,release dispatch 按需。 -> **调研存档**(ctx_search 按 source 检索):atomcode-q2-multistrategy / atomcode-q3-vps-mgmt / atomcode-q4-perf-budget / atomcode-q5-debt / atomcode-q6-governance。 -> **Wave 状态(2026-09-20 更新)**: Wave A(R11-00/02/04/10/11)已 landed origin/main @ 17866d37(verify 绿)。Wave B 执行任务书=docs/agents/round11-wave-b-next-round.md(r11-wave-b grill D-001 拍板:wave B 照单全收 + 新增 R11-14「CI 门+卫生」,规格定义在 wave B 任务书)。 -> **Wave C 任务书(2026-09-20 定稿)**:docs/agents/round11-wave-c-next-round.md(r11-wave-c grill D-001..D-005:R11-06/07/12/13 照单全收 + 新增 R11-15 卫生 / R11-16 删 auto-clean(ADR-0073 删除权威)/ R11-17 治理减法;基线 origin/main @ 4b19d8fe)。 - -## 任务清单 - -### R11-01: AGENTS.md 双铁律仲裁 + ADR-0072(治理,小票)✅ 已于整理阶段完成 - -- **覆盖决策**: D-006#1 -- **已落地**: P23 硬闭环三处删除并重写为 CI-only close-loop;ADR-0072 记录转型决策(与本任务书同一提交系列) -- **余量(残留项)**: 若 CI release job 未承载 chunk-hash 校验(现仅在 build-all 脚本内),把 T10 hash guard 移植/挂接到 CI 产物校验(半天) -- **风险**: 低 - -### R11-02: 根目录盘点 + 归档修正(治理,小票) - -- **覆盖决策**: D-006#2、#5;吸收 Round10 backlog P1-②(归档报告 2 处 "43" 残留清理) -- **范围**: ~~.scratch 移出追踪~~(2026-09-19 事实修正:`.scratch/` 自始被 `.gitignore:100` 忽略、`git ls-files .scratch`=0,D-006#2 的 untrack 机制无对象,意图由现状满足);`repro/`、`bench-results/`、`npm/` 逐个处置(归位 tests/fixtures 或 scripts/,或移出追踪);`llms.txt` 保留(AI agent 事实标准);修正归档报告中 2 处 "43" 计数残留 -- **工作量**: 半天;**风险**: 低(纯 git 索引/文档,不删文件) - -### R11-03: headless 管理面对等补齐(P0 主票) - -- **覆盖决策**: D-003(全部)+ D-006#4a -- **范围**: ①修复 `/api/v1/ports/{port}` axum 0.7 字面路径 bug(Round10 backlog P1-① 吸收);②CMD_TO_HTTP 40 → SPA 可达功能全覆盖(shell 侧命令由 BFF 原生实现,非透传);③新增 `GET /api/v1/capabilities`(BFF 原生端点,不增 IPC manifest);④DISABLED_COMMANDS 44 条按判据重裁(仅桌面环境假设类可禁,纯控制面命令转正);⑤`X-Forwarded-Proto` scheme 检测 + CSP `frame-ancestors none`;⑥HTTP 冒烟脚本(非浏览器,断言新路由)挂入 CI verify job -- **工作量**: 3-5 天;**风险**: 中(BFF 面广);**依赖**: 建议 R11-04 先行 -- **验收**: capabilities 端点返回机器可读清单;禁用清单缩小到桌面专属;冒烟脚本在 CI 绿 - -### R11-04: ipc.ts 边界 preparatory 拆分(R11-03 前置) - -- **覆盖决策**: D-005⑤b -- **范围**: 从 ipc.ts(2326 行)抽出路由表(CMD_TO_HTTP)/capabilities/guard 三块为独立模块——只拆 R11-03 需要的边界,不做全量大扫除(rabbit-hole 警告) -- **工作量**: 半天-1 天;**风险**: 低-中(行为不变,测试锁) - -### R11-05: 策略诚实化 + 模板 + 区域级近似 - -- **覆盖决策**: D-002(A + C) -- **范围**: UI 如实呈现三策略(BALANCED / PREFER_LOW_LATENCY / PREFER_IDLE_IP);策略模板/预设——经 ADR-0036/0052 权威写入口的预置快照,一键应用到平台(generation bump + 审计同行);A-class top-N 落地为区域集的语义明示化;i18n ×18 同步 -- **工作量**: 2-3 天;**风险**: 低-中 - -### R11-06: 平台级策略编排 controller - -- **覆盖决策**: D-002(B)+ D-005⑤a -- **范围**: 先拆 strategy_service(2927 行)的编排边界(不做 7 职责全分解);条件自动化状态机:风控信号 → probe 候选 → 换区/换策略 → 观察期 → 固化/回滚,含 ejection 冷却防抖;复用 Resin probe(probe-egress/probe-latency)与 platform PATCH API;衔接 R10 票 08 的 Inline-Polling driver 模式 -- **工作量**: 3-5 天;**风险**: 中;**依赖**: R11-05(语义先行) -- **验收**: 状态机有单测;防抖参数可配;切换全程走权威写入口 + 审计 - -### R11-07: key→账号→绑定映射查询面 - -- **覆盖决策**: D-002(D 退化路径) -- **范围**: 按 API key 反查所属账号与其绑定出口 IP/端口的查询能力与 UI 呈现(不解析请求体) -- **工作量**: 1 天;**风险**: 低 - -### R11-08: SQLite 写路径修复 - -- **覆盖决策**: D-004 + D-005② -- **范围**: replace_ports 全局锁修复——WAL + BEGIN IMMEDIATE + busy_timeout ≥5s,写事务保持极短(在 ADR-0011 Mutex+spawn_blocking 模式上演进) -- **工作量**: 1 天;**风险**: 中低(db 层,测试齐全) -- **验收**: 控制面写操作 p99 ≤50ms(D-004 验收线) - -### R11-09: 数据面测试扩展 - -- **覆盖决策**: D-004 -- **范围**: forwarder 真 socket 测试补 SSE 双指标断言(首 token 增量 ≤1ms、帧不合并/frames-per-arrival)+ 新增延迟 p99 ≤10ms;500 并发 SSE 流内存有界;现有 p95 ≤5ms 断言维持 -- **工作量**: 1 天;**风险**: 低(真 socket 测试模式已存在) -- **注意**: 共享 runner 不加硬绝对门;内存/并发绝对值归 R11-12 基线实测 - -### R11-10: gen_token 统一熵源 + TOCTOU 修复 - -- **覆盖决策**: D-005①③ -- **范围**: sidecar.rs gen_token()(SystemTime+pid)→ getrandom CSPRNG(对齐 headless_security;安全债 High/CWE-330/340,SLA 无协商);pick_free_loopback_port bind→drop→spawn 竞态修复 -- **工作量**: 半天;**风险**: 低 - -### R11-11: 卫生·触碰即换 - -- **覆盖决策**: D-005④⑦ + D-007 -- **范围**: 本轮触碰模块的手写编解码换 crate(b64/hex/urlencoding;civil_from_days 优先——off-by-one 高发区);items_arr 三处去重;PBKDF2 主线程存疑核验(backup.rs KDF 常量,若在则 spawn_blocking 化);本轮触碰文件中的 Txx/roundN/handoff 历史注释随触碰清理(改为 phase-history 指针或删除) -- **工作量**: 半天-1 天;**风险**: 低 - -### R11-12: 部署文档 + 基线实测 - -- **覆盖决策**: D-003(部署文档部分)+ D-004 -- **范围**: HEADLESS_DEPLOYMENT.md 增补——TLS 反代指引、零信任组网推荐项(Tailscale/WireGuard,非依赖)、FD ≥8192 与内核 socket 缓冲(~174KB/连接)预算表、1C1G 最低规格;headless 全栈 RSS 基线实测(目标总 ≤150MB / 空闲 ≤80MB)——用 CI 产物在代表性硬件测,结果入 PERF-BENCH.md -- **工作量**: 1 天;**风险**: 低(实测需用户硬件或发版窗口) - -### R11-13: 轮末评估与立票 - -- **覆盖决策**: D-005⑥⑧ + D-006#4b -- **范围**: (a) React Query 迁移触发评估——R11-03 落地后清点手写轮询点位,≥3 且均为 server state 快照则启动迁移专项;(b) 真 WebView 冒烟 E2E 立票(5-15 条关键路径,CDP 模式 tauri-plugin-playwright / tauri-driver,Windows/Linux CI 矩阵,post-merge/nightly;macOS 无桌面 WebDriver 缺口文档化);(c) tray i18n 构建期生成挂起确认(下次 i18n 需求触发) -- **工作量**: 半天;**风险**: 低 - -## 波次建议(由依赖推导,非强制) - -- **波次 A(可并行)**: R11-02、R11-04、R11-10、R11-11 -- **波次 B**: R11-03(R11-04 后)、R11-05、R11-08、R11-09 -- **波次 C**: R11-06(R11-05 后)、R11-07、R11-12 -- **波次 D(轮末)**: R11-13 - -## 范围外记录(显式,无需立票) - -| 项 | 理由(来源) | -|---|---| -| Resin fork 线 / 节点级 pin | D-001/D-002:触发条件「同一账号内部、单请求按 model 字段动态换出口」未命中 | -| 应用层前置网关(LiteLLM 模式) | D-002:远期可选组件;Mode B 双跳延迟与低延时目标冲突 | -| 桌面客户端远程管理 | D-003:三触发条件均未命中 | -| API-first(弃完整 UI) | D-003:不采纳 | -| strategy_service 7 职责全分解 / TopologyView 拆分 | D-005⑤:只拆特性边界;⑤c churn 触发;Zustand shallow 空对象(TopologyView.tsx:76,r11-wave-b D-002③)并入 ⑤c 同批(触碰时先实测再修) | -| React Query 全迁移 | D-005⑥:R11-13 触发评估 | -| tray i18n 构建期生成 | D-005⑧:下次 i18n 需求触发 | -| i18n 收缩 | D-006#3:维持 18 语言;locale 分布触发 | -| 竞品对标基准 | D-004:fork 线立项时的证据手段 | -| Mode B 数据面性能预算 | D-004:Resin 领域,壳层不设 | -| SSRF guard 统一 | 账本对账闸 #4:threat model 不同,暂不动 | -| R9 D-006 观察哨 / R9 D-008 导入原子性 | 前轮 deferred,触发未命中,维持 | - -## Suggested skills - -| Ticket | Skills | -|---|---| -| R11-02 / R11-10 / R11-11 | implement(tdd) / code-review | -| R11-03 / R11-04 | to-tickets / implement(tdd) / code-review | -| R11-05 / R11-06 | to-spec / to-tickets / domain-modeling / implement(tdd) / code-review | -| R11-07 | to-tickets / implement(tdd) | -| R11-08 / R11-09 | implement(tdd) / code-review | -| R11-12 | handoff / atomcode-research(先 ctx_search 存档再开新调研) | -| 通用 | but(版本控制唯一面)/ ctx_*(工具路由,文件编辑走 ctx)/ codegraph(探索先查)/ i18n-check(涉 UI 文案必跑)/ ADR-0072(CI-only,禁本机构建) | - -## 数据源引用 - -- 账本:`.scratch/round11-grill/decision-ledger.md`(D-001..D-007 current) -- 本轮 ADR:`docs/adr/0071-headless-parity-policy.md`、`docs/adr/0072-ci-only-build-transition.md` -- 前轮归档:`.scratch/architecture-recovery-closed-2026-09-18/`(Round 10,A-001..A-012 已结算)、`.scratch/round9-grill/decision-ledger.md`(R9 D-001..D-010) -- 上游研究:`docs/research/RESIN_ROUTING_ARCHITECTURE_RESEARCH.md`(Resin 无 per-node API 证据) - ---- - -**生成时间**: 2026-09-19 Round 11 grill 整理阶段 -**对账闸**: 通过(无去向记录清单为空;D-007 为账本外结论经用户拍板后补记) diff --git a/docs/agents/round11-wave-b-next-round.md b/docs/agents/round11-wave-b-next-round.md deleted file mode 100644 index 83a64f2a..00000000 --- a/docs/agents/round11-wave-b-next-round.md +++ /dev/null @@ -1,89 +0,0 @@ -<!-- - Source-of-truth path: .scratch/r11-wave-b-grill/handoffs/next-round.md (session artifact, gitignored by .gitignore:100) - This docs/agents/ copy is the persistent, git-tracked reference. - Generated: r11-wave-b grill closeout (D-001/D-002 settled; Wave A completion audit in ledger appendix A). - Regenerate this file from the session artifact after any ledger update. ---> -# Round 11 Wave B — Resident Task Book(r11-wave-b grill 收口) - -<!-- - Source-of-truth ledger: .scratch/r11-wave-b-grill/decision-ledger.md (D-001..D-002 current, 2 entries) - Git-tracked mirror: docs/agents/round11-wave-b-next-round.md - Parent ticket registry: docs/agents/round11-next-round.md (R11-01..13; R11-14 defined HERE) - Generated: 2026-09-20 wave B grill closeout (post-Wave-A execution round) - Regenerate this file from the session artifact after any ledger update. ---> - -## 对账闸(2026-09-20,通过) - -| 记录 | 去向 | -|---|---| -| D-001 | 本任务书执行令(wave B = R11-03 P0 + R11-05 + R11-08 + R11-09 + R11-14 新票);wave C/D 顺延维持 | -| D-002① | R11-14 票内执行(WHY-NOT-EFFECTIVE troubleshooting 行 + 观察哨,不改代码) | -| D-002② | ADR-0071「已知取舍」句——已于整理阶段落入 docs/adr/0071-headless-parity-policy.md | -| D-002③ | round11 任务书范围外表 ⑤c 注记——已于整理阶段落入(.scratch 与 docs/agents 两份拷贝同步) | - -无去向记录清单:**空**。 - -## 执行令(D-001) - -Wave B 照单全收,R11-03 为 P0 主票。票据规格一律按引用读 docs/agents/round11-next-round.md 对应章节,此处不复制: - -| 票 | 规格引用 | 覆盖决策 | -|---|---|---| -| R11-03(P0) | round11 任务书 §R11-03:/api/v1/ports/{port} 字面路径修复、路由补齐 SPA 可达对等、GET /api/v1/capabilities、DISABLED_COMMANDS 重裁、X-Forwarded-Proto + CSP frame-ancestors、HTTP 冒烟入 CI verify | round11-grill D-003 + 本轮 D-001 | -| R11-05 | round11 任务书 §R11-05:策略诚实化 + 模板(权威写入口预置快照)+ 区域级近似 + i18n ×18 | round11-grill D-002(A/C) + D-001 | -| R11-08 | round11 任务书 §R11-08:WAL + BEGIN IMMEDIATE + busy_timeout ≥5s;验收 p99 ≤50ms | round11-grill D-004/D-005② + D-001 | -| R11-09 | round11 任务书 §R11-09:SSE 双指标断言 + 延迟 p99 ≤10ms + 500 并发内存有界 | round11-grill D-004 + D-001 | -| R11-14(新) | 见下节 | 本轮 D-001 + D-002① | - -依赖:R11-04 已 landed(src/lib/headless-routes.ts 129 / headless-availability.ts 168 / headless-dispatch.ts 104 就位),R11-03 可直接开工。 - -## R11-14(新票):CI 门 + 卫生 - -- **覆盖决策**: 本轮 D-001(审计移交项)+ D-002① -- **硬交付(CI 门,verify job / verify-build.sh 内)**: - 1. lockfile-diff 门:Cargo.lock 与 Cargo.toml 失同步即红(F1 复发防护;`cargo metadata --locked` 或等价只读 lockfile 检查,不产生构建产物) - 2. `cargo fmt --check` 门(fmt 残留持续流出问题) -- **随触碰卫生清单(本轮触碰面内顺手清,不专程扫全库)**: - - vacuous check_port_available 测试(wave-a audit §5) - - 死 ALLOCATION_POLICIES 导出(同上) - - `(ADR-0016 )` 空括号注释(同上) - - 重复目录解析块(同上) - - isTauri 诊断探针常驻生产 bundle(src/lib/headless-availability.ts:39;R11-03 触碰该文件时 dev-only 化或删除) - - src-tauri/src/headless_main.rs:633 url_encode_segment 随 R11-03 触碰评估换 crate(resin_client.rs:789/812 两个编码器语义有意不同且各带测试——先读测试再动,语义不合并) -- **文档顺手项(D-002①,不改代码)**: docs/how-to/WHY-NOT-EFFECTIVE.md troubleshooting 表补一行——watch_apply 回滚窗口:文件已落盘而 apply 失败时内存回滚、磁盘保留新内容;authoritative_snapshot 显示 Drifted、reconcile_now 可自愈;**勿「加固」成磁盘回滚——会吞用户手改的 L2 权威内容** -- **不触碰**: TopologyView.tsx(D-002③ 归 ⑤c 触发器);Resin API 边界(D-001 约束) -- **工作量**: 半天-1 天;**风险**: 低 - -## 本轮不做(顺延/范围外,均维持既有决策,零推翻) - -- Wave C/D:R11-06(编排 controller)、R11-07(key→账号映射)、R11-12(部署文档+基线实测)、R11-13(轮末评估)——wave B 完成后按 round11 任务书波次推进 -- fork 线、应用层前置网关、桌面远程客户端、API-first、strategy_service 全分解、React Query 迁移、tray i18n、i18n 收缩、竞品对标、Mode B 预算、SSRF 统一、R9 D-006/D-008——理由表见 round11 任务书「范围外记录」 - -## Wave A 后现状基线 - -锐评完成度审计结论(细节与行号证据见账本附录 A):已完成 11 项(gen_token 熵源 / TOCTOU / b64 / hex / civil_from_days / items_arr / Txx 注释 / 根目录清理 / AGENTS.md 仲裁 / ipc.ts 拆分 / 计数修正),驳回 1 项(PBKDF2 已 spawn_blocking 卸载),其余剩余项全部已映射到本任务书票号。上下文恢复链:.scratch/r11-wave-a/handoffs/wave-a-audit-handoff.md → 本账本附录 A → docs/agents/round11-next-round.md。 - -## 执行纪律 - -- but 唯一版本控制面;每票独立 lane 分支;push 触发 CI verify(ADR-0072:交付证据=CI 产物,本机零构建);land 前 CI 绿,but land --whole-stack(wave-a 先例);轮末合并后停住汇报。 -- 文件编辑走 ctx(node fs);CodeGraph 探索先查、改后 codegraph sync .;i18n 键变更 ×18 同 commit(pnpm i18n:check)。 -- 每票收尾 code-review;R11-03 路由语义开工前可先 to-tickets 细化。 - -## Suggested skills - -| 阶段 | Skills | -|---|---| -| 开工 | but / codegraph(探索) | -| 每票 | implement(tdd) / code-review;R11-03 可先 to-tickets | -| R11-05 | domain-modeling(策略模板术语如需精化)+ i18n-check | -| 收尾 | handoff(生成 wave C 恢复上下文) | -| 通用 | ctx_*(工具路由,文件编辑走 ctx)/ ADR-0072(CI-only 禁本机构建)/ but | - -## 数据源引用 - -- 账本:.scratch/r11-wave-b-grill/decision-ledger.md(D-001..D-002 current + 附录 A 审计快照) -- 票据注册表:docs/agents/round11-next-round.md(R11-01..13) -- Wave A 审计:.scratch/r11-wave-a/reports/2026-09-19-audit.md + .scratch/r11-wave-a/handoffs/wave-a-audit-handoff.md -- ADR:docs/adr/0071-headless-parity-policy.md(本轮补已知取舍句)、docs/adr/0072-ci-only-build-transition.md diff --git a/docs/agents/round11-wave-c-next-round.md b/docs/agents/round11-wave-c-next-round.md deleted file mode 100644 index ab07264f..00000000 --- a/docs/agents/round11-wave-c-next-round.md +++ /dev/null @@ -1,99 +0,0 @@ -<!-- - Source-of-truth path: .scratch/r11-wave-c-grill/handoffs/next-round.md (session artifact, gitignored by .gitignore:100) - This docs/agents/ copy is the persistent, git-tracked reference. - Generated: r11-wave-c grill closeout (D-001..D-005 settled; 2 atomcode research rounds). - Regenerate this file from the session artifact after any ledger update. ---> -# Round 11 Wave C — Handoff / Task Book(r11-wave-c grill 定稿版) - -> 来源:r11-wave-c-grill(2026-09-20 定稿)。决策账本(唯一数据源):`.scratch/r11-wave-c-grill/decision-ledger.md`(D-001..D-005,全 current)。 -> 本文件取代返工窗口起草的 `.scratch/r11-wave-c/handoffs/next-round.md`(修正其两处偏差:观察项未入任务书 → 已立 R11-15/16/17;引用不存在的 `.scratch/r11-wave-b-grill/decisions/` → 实际为 `decision-ledger.md`)。 -> 基线:`origin/main @ 4b19d8fe`(wave-B 14 提交线性史;main verify 35494598357 绿,五门实测执行)。 -> git-tracked 镜像:`docs/agents/round11-wave-c-next-round.md`。 - -## 执行纪律(wave-B 审计教训,D-001 约束列) - -- 逐 lane `but land`;land 后 `git fetch` 复核 `origin/main` tip + `git cat-file -e` 树内容,再写报告。 -- CI-only(ADR-0072):本机零构建;每源码提交必有 CI verify 证据。 -- i18n 改动 ×18 同 commit;权威写入口不可旁路;Resin vendored 不动。 -- 叙事 token(R11-xx / wave-x / grill)禁止回灌生产代码(wave-B F4 教训)。 -- 新 IPC 命令一律走 `ipcInvoke` 调度口 + capabilities registry + manifest check(R11-03 纪律,wave-B F2 教训)。 -- ADR-0062 D5 lockstep 仅适用于新增端点。事实修正(账本过程注记):`probe_node_egress` / `probe_node_latency` / metrics history probes 均已接线(resin_client.rs:381/393/647)——R11-06 直接复用,无需新增 ResinClient 方法。 -- 报告:`.scratch/r11-wave-c/reports/<date>-report.md`;波末节写真实落态(逐 lane sha + 分支清理状态)。 -- 轮末归档节奏(D-005③,随 R11-17 执行):已完结目录(r11-wave-a/b、round11-grill、r11-wave-b-grill、r11-wave-c-grill)统一挪入 `closed-2026-09-XX/`,顶层 20 → ≤12。 - -## 票据(依赖序) - -### R11-16 — 删 auto-clean:期望态删除权威落地【覆盖 D-002;ADR-0073 已立法】 - -- 移除 `PlatformsView.tsx:120` `cleanedPlatforms` 过滤(含 `setStrategyConfig(cleanedConfig)` 分支);`config_put` 原样提交完整期望态,绝不以 L3 读取结果裁剪条目。 -- 回归测试锁两不变式:① 策略字段同步不得缩减平台条目集合;② 任何以实况列表为输入的写回在实况为空时硬性拒绝(allowEmpty 语义,防 argo-cd#23645 类全量误删)。 -- 「白盒有、live 无」交给既有 Drifted 相位 + `reconcile_now` 自愈——复用基础设施,零新配置键。 -- 未来钩子(本轮不做):reconcile 预览式 Prune=confirm 清理入口(显式确认 + audit op + 备份环)。 -- 建议最先落地:小而独立,先锁安全不变式再动后续特性。 - -### R11-06 — 平台级策略编排 controller(P0 主票)【覆盖 D-001、D-003】 - -- 范围:拆 `strategy_service`(2900+ 行)编排边界(非 7 职责全分解,round11 D-005 ⑤a);状态机=风控信号→probe 候选→换区/换策略→观察期→固化/回滚,ejection 冷却防抖。 -- 分级混合自动化(D-003):同一状态机 + per-action 执行闸。动作面=可逆 PATCH(region_filters / allocation_policy);大动作(删除/备份回滚/跨平台联动/账号绑定变更)永不在控制器曲目。auto/suggest=两档 autonomy(suggest 下 transition 停在待确认态并出 diff,人批准才 apply)——不是两套代码路径。默认档:headless=auto(保守参数包),桌面=suggest。 -- 六不变式参数包(全部可配、出厂保守):连续失败阈值 / 滑动窗口最小样本量 / 失败率+慢调用双阈值(慢调用必须算数——风控常表现为挑战/超时非显式 5xx)/ 容忍带相对阈值(新 region 须好 ≥20% 或错误率差 ≥2×)/ 观察期连续 M 成功周期才固化且任一变差重新计时 / 冷却指数退避封顶(Envoy 式)/ 单轮最大切换比例(防全平台雪崩)/ min_switch_interval + 全局 max_switches_per_hour。 -- 信号源:端到端 `probe_exit_ip` + `port_health_check` 为主;节点级复用既有 ResinClient probe 方法(见事实修正行);IP 信誉仅加权(配 key 才启用)。 -- 执行面:切换必走权威写入口 config_put→apply(generation + audit);全部 transition 落 audit.jsonl 供调参;升档纪律:先 suggest 运行收集 audit 数据再放宽默认档。 -- 配置归属(票据内定,约束已立):L2(三层立法排除 L1/L3)——推荐 `egressapikey-strategy.json` 新增可选 `orchestration` 节(serde 缺省=off,同备份环同审计),不加第三权威写入口。 -- 新 IPC 命令(orchestration 状态/配置)走调度口 + registry + manifest check。 -- spec 以 vendored resin 源码为准(调研中 per-node circuit-breaker/EWMA 表述未核实,不作为依据)。 -- 验收:状态机单测(含两档 autonomy 与 suggest 停确认态);防抖参数可配;切换全程权威写入口 + 审计;不引入新数据面行为。 -- 立法:实现 lane 内写本票 ADR(分级自动化 + 六不变式,引用 D-003 调研来源)。 - -### R11-12 — 部署文档 + 双层基线【覆盖 D-001、D-004】 - -- HEADLESS_DEPLOYMENT.md 增补:TLS 反代指引、零信任组网推荐(Tailscale/WireGuard 非依赖)、FD ≥8192 + 内核 socket 缓冲(~174KB/conn)预算、1C1G 最低规格。 -- 双层基线(D-004,执行面统一 GitHub Actions): - - ① github-hosted Linux runner 一次性记录型基线(无门):SSE 首 token p99 / 500 并发 RSS / 延迟 → 新建 `docs/PERF-BENCH.md`「shared-runner 参考列」。 - - ② self-hosted runner 代表性绝对基线:用户目标硬件(1C1G Linux VPS 优先,本机 Windows 兜底)注册 runner;baseline workflow 仅 `workflow_dispatch` 手动触发;跑 CI 构建产物;数字上传 artifact + 回填 PERF-BENCH.md「代表性硬件列」——该列是 RSS ≤150MB / ≤80MB 钉数与绝对门收紧的唯一依据。 - - 安全轨(仓库 PUBLIC):baseline workflow 仅 workflow_dispatch;任何 pull_request/push 触发的 job 禁止引用 self-hosted label。 -- wave-B 校准项:SSE 首 token p99(票线 1ms,Windows 实测 1.14ms——CI Linux 数值需基线);500 并发 RSS 绝对门;shared-runner 延迟基线。现断言形态 median≤1ms + p99≤3ms / 512KiB/conn——基线后收紧。 -- round11 D-004 立法不变:共享 runner 不加硬绝对门。 -- 外部取证:atomcode-research(TLS 反代 / 零信任组网,串行单在飞)。 - -### R11-07 — key→账号反查面【覆盖 D-001;round11 D-002 D】 - -- 按 API key 反查所属账号 + 绑定出口 IP/端口;不解析请求体;只读 UI 呈现。 -- headless parity:新 IPC 命令走调度口 + capabilities registry + manifest check。 -- 1 天,低风险。 - -### R11-15 — 卫生小票【覆盖 D-001】 - -- patchAndSync React updater + 微任务竞态;HeadlessStore last-writer-wins;`proxy_to_resin` 每请求新建 reqwest::Client;`query_param` 同名两义(headless_main vs headless_security——改命名或注释锁定语义,先读测试);远端旧分支 `fix/zcode-GUI-Topology` 删除。 -- 每项独立小 commit;触碰即清。 - -### R11-17 — 治理减法 + guard 补落地【覆盖 D-005】 - -- ① AGENTS.md 回落 ≤32KiB:ipc-manifest 全量块(~8KB 机器生成清单)外移 docs/agents/(正文留指针 + 计数,生成源 `scripts/ipc-manifest-check.cjs`)+ Storage locations 细节外移(渐进披露惯例)。不得弱化 context-mode 路由块与 CodeGraph 块(保持置顶);外移内容 + 指针链接同 commit(dead-link 门)。 -- ② verify-build.sh 补 AGENTS.md ≤32KiB size guard(Phase 6 声称但未落地;实测磁盘 ~51KB,较 Phase 5 目标 21.5KiB 回弹 2.4×)。 -- ③ .scratch 归档:见执行纪律末条。 -- 约束:18 语言不裁;ADR/docs 知识不删;不加新治理工具/流程。 - -### R11-13 — 轮末评估与立票【覆盖 D-001】 - -- (a) React Query 迁移触发评估(清点手写轮询点位,≥3 且均为 server-state 则立专项)。 -- (b) 真 WebView 冒烟 E2E 立票(CDP tauri-driver,Windows/Linux 矩阵 post-merge/nightly;macOS 缺口文档化)。 -- (c) tray i18n 构建期生成挂起确认(下次 i18n 需求触发)。 - -## 范围外(显式,账本依据) - -- fork line:触发条件维持唯一条(round11 D-002「同一账号内部、单请求按 model 字段动态换出口」),本轮不触碰;壳层不重返数据面内核(ADR-0050)。 -- Mode A 独立 crate(锐评1 建议):deferred 无主,不立项。 -- 18 语言裁剪 / docs 大合并 / ADR 删减:D-005 显式不做。 - -## suggested skills(下一 Agent) - -| 票 | skills | -| --- | --- | -| R11-16 / R11-07 | implement(tdd:先锁两不变式/只读契约)+ code-review | -| R11-06 | implement(tdd:状态机单测先行)+ code-review;lane 内写 ADR | -| R11-12 | atomcode-research(TLS/零信任取证,串行)+ implement | -| R11-15 / R11-17 | implement + code-review(触碰即清) | -| R11-13 | research / triage(评估产出立票建议) | -| 全程 | gitbutler(but 唯一写面;lane 配方先读技能) | -| 轮末 | neat-freak + handoff + code-review(审计双轴) | diff --git a/docs/agents/round12-next-round.md b/docs/agents/round12-next-round.md deleted file mode 100644 index 8df7cd6b..00000000 --- a/docs/agents/round12-next-round.md +++ /dev/null @@ -1,68 +0,0 @@ -# Round 12 任务书(wave C 轮末评估产物,R11-13) - -> 来源:round 11 wave C 收束评估(2026-09-21)。本文件由 R11-13 票产出: -> 轮末清点 → 立票。上游任务书:docs/agents/round11-wave-c-next-round.md; -> 决策账本:.scratch/r11-wave-c-grill/decision-ledger.md(D-001..D-005)。 - -## Wave C 收口快照 - -| 票 | 落点 | CI | -| --- | --- | --- | -| R11-16 删 auto-clean(D-002/ADR-0073) | `5da19d3b` — `cleanedPlatforms` 过滤移除 + 双不变式回归测试 | verify `35609687230` 绿 | -| R11-15 卫生 ×6 + 远端分支删除 | `fc7cf134`/`1da10a18`/`4af1922b`/`0e24c4da`/`340206ea`/`e0b0ed71`/`5968c3cb`/`f92668d1` | verify 绿(fmt/tsc 两轮修复后) | -| R11-07 key→账号反查 | `1dc4ffae` + fmt `c8ca1b2d` | verify `35614840192` 绿 | -| R11-17 治理减法 + guard | `7ee61023` — AGENTS.md 51KB→32.3KB + size guard + manifest 外移 + .scratch 归档 | verify `35615897698` 绿 | -| R11-06 编排 controller(D-003/ADR-0074) | `60e1f8bb` + `242e194d`/`9a231013` 修复 | 见当轮 verify 记录 | -| R11-12 部署文档+双层基线 | `242e194d`(文档+workflow 随 R11-06 修复 commit 一起落地,票据归属混合已披露) | perf-bench `35619850050` 记录型基线 | - -## (a) React Query 迁移触发评估 — 结论:立专项 R12-01 - -手写轮询点位清点(全部 server-state): - -| 点位 | 机制 | 语义 | -| --- | --- | --- | -| `appStore.subscribeToConverge` | `setTimeout` 链(5s/30s) | 全局快照收敛 | -| `TopologyView.sync` | usePoll 5s | 拓扑快照 | -| `NodesView.refresh` | usePoll 10s | 节点列表 | -| `DiagnosticsView.refreshDiagnostics` | usePoll 动态间隔 | 诊断 | -| `SubscriptionsView.pollEdge` | usePoll 30s | 边缘状态 | -| `PlatformsView.refreshOrch` | usePoll 15s(R11-06 新增) | 编排待批提案 | -| 各 view mount 拉取 + `HeadlessStore` | 一次性 fetch | server-state 首载 | - -清点结果:**≥3 且均为 server-state → 触发条件命中,立票**。但注意收敛现状——轮询已统一在 `usePoll` 钩子(usePoll.ts:1-98)+ `convergeSubscribe` 单缝之后,迁移面 = 替换 usePoll 内部实现 + converge 订阅 + 挂载拉取,而非逐点重写。 - -### R12-01 — React Query 采用评估与(可选)迁移 - -- **范围**:把 usePoll/convergeSubscribe/mount-fetch 三类 server-state 面映射到 React Query(queryKey 规范、staleTime 替代手写 interval、refetchOnWindowFocus 替代 visibilitychange、mutation 替代手写 patchAndSync 链)。 -- **判定前置**:先回答 React Query 在 Tauri WebView + headless BFF 双通道下的 invoke 包装形态(queryFn = ipcInvoke),以及 SettingsView 的 debounce 写入是否属于 mutation。 -- **约束**:不引入第二个数据层;usePoll 对外签名保持兼容或一次性全量替换;headless dispatch 路径不变。 -- **工作量**:评估 0.5 天;若迁移 2-3 天;**风险**:中(收敛时序敏感——`convergeSubscribe` 的 5s/30s 节奏是漂移检测语义的一部分)。 - -## (b) 真 WebView 冒烟 E2E — 立票 R12-02 - -现状:`e2e/` 5 个 Playwright spec 全部打 vite dev server(playwright.config.ts `webServer: pnpm dev`),**不覆盖真 WebView/Tauri 运行时**——IPC 桩、tray、sidecar 生命周期、窗口行为零覆盖。 - -### R12-02 — WebView 冒烟 E2E(tauri-driver / WebDriver) - -- **范围**:tauri-driver + WebDriverIO(或 CDP 直连)对 **CI 构建产物** 跑冒烟集:窗口拉起、`MainWindowTitle=="EgressAPIKEY"` 活性断言(沿用 ADR-0072 测活协议)、nav 切页、至少一条 IPC 往返(如 `port_list`)。 -- **矩阵**:windows-latest + ubuntu-latest(WebKitGTK),post-merge/nightly 触发(不进 PR 门);macOS 缺口显式文档化(runner 许可 + tauri-driver macOS 限制)。 -- **约束**:CI-only 执行(ADR-0072);冒烟集 ≤5 分钟;失败只报不拦(首轮稳定后再升 gate)。 -- **工作量**:1-2 天;**风险**:低-中(CI 环境 WebView2/WebKitGTK 依赖安装是主要摩擦)。 - -## (c) tray i18n 构建期生成 — 维持挂起 - -现状:`src-tauri/src/tray.rs` 静态表覆盖 18 locale,靠「同一 commit 同步 + i18n-check 门」手工 lockstep(tray.rs:60-62 注释约束)。构建期从 frontend catalog 生成的方案**继续挂起**——当前双目录(JSON catalog × Rust 表)靠门控守住一致性,零事故记录;触发条件维持不变(下一次 i18n 需求或 lockstep 事故)。 - -## R12-03 — 编排信号面补全(审计 F-1) - -- **现状弱化**:tick verdict 仅由 `port_health_check`(loopback TCP+SOCKS5 greeting)推导——不过真实出口,上游烧号/风控挑战(D-003 的动机场景)对控制器不可见;`probe_exit_ip`/`probe_node_egress`/`probe_node_latency`/metrics-history 均已存在但未接入信号面。 -- **范围**:把端到端出口探针接入 collect_verdict(bound port → 经该 port 的 exit probe,慢调用/超时算失败——风控常以挑战/超时呈现);复核 `list_nodes` 的 `failure_count==0` 判 ok 口径(若为累计值会永久惩罚有史 region、粉饰未用 region——需要窗口化或 EWMA 口径)。 -- **约束**:不引入新数据面行为;复用既有 ResinClient 方法;信号环仍进程内内存;每 tick 探针数有界(按 bound port 数封顶)。 -- **工作量**:1-2 天;**风险**:中(探针成本×tick 频率的乘积要有界)。 - -## 移交项(非立票,记录) - -- `strategy_service.rs` ~2900 行 7 职责——R11-06 只加了 `orchestration_mutate` 状态写,未拆职责(任务书范围外)。 -- `TopologyView.tsx` ~1271 行——⑤c 触发线维持(churn 触发再拆)。 -- converge 模块级全局——R11-13a 原点位,已由本评估 (a) 并入 R12-01 的迁移面描述。 -- self-hosted runner 注册(R11-12 ② 的用户侧动作):`bench-selfhosted.yml` 已就位,等用户在目标硬件注册 runner 后 workflow_dispatch 触发,数字回填 PERF-BENCH.md 代表列。 diff --git a/docs/agents/round12-wave-a-next-round.md b/docs/agents/round12-wave-a-next-round.md deleted file mode 100644 index 626ab945..00000000 --- a/docs/agents/round12-wave-a-next-round.md +++ /dev/null @@ -1,68 +0,0 @@ -# Round 12 Wave A 任务书(r12-wave-a grill 定稿产物,2026-09-22) - -> 唯一数据源:`.scratch/r12-wave-a-grill/decision-ledger.md`(D-001..D-005 全 current) -> 上游输入:`docs/agents/round12-next-round.md`(候选票书,本文件为其 adjudicated 版)+ `.scratch/r11-wave-c/handoffs/2026-09-22-audit-handoff.md` + `.codex-tmp/锐评{1,2}.md` -> 取证与调研:五轮 atomcode 深度调研(session 锚点见账本过程注记),全部结论与既往 current 决策同向,零 revised。 - -## §0 轮次形态(覆盖 D-001) - -本轮为**目标轮**:验收线(§1)先行立法,全部候选票过线过滤重排优先级。过滤规则四档(已立法):直通验收线→升档;承保路径依赖→保留/并入;与线无因果但有独立价值→降档留票面;纯结构债→挂起待触发线。 - -## §1 部署剖面验收线 spec(覆盖 D-002) - -双部署剖面:**低配 VPS headless Linux**(1C1G 级,egressapikey-headless+sidecar,无人值守)/ **普通 Windows 桌面**(Tauri GUI+WebView2 进程树+sidecar)。 - -| 指标 | 口径 | 目标阈 | 危险阈 | 现状(shared-runner 参考列) | -| --- | --- | --- | --- | --- | -| SSE TTFB 附加 delta | paired p95 | ≤5ms | ≤30ms | 26.2ms | -| SSE event gap 附加 delta | 好事件占比 | ≤10ms @≥99% | ≤500ms @≥99% | Mode A 待测;Mode B 4619ms(登记豁免) | -| idle RSS 全进程树 | 绝对 | ≤80MB † | ≤120MB † | 95.6MB | -| soak RSS @200 SSE 流 | 绝对 | ≤128MB † | ≤250MB † | 85.9MB | -| paired 附加延迟 | delta p99 | ≤1ms | ≤3ms | 0.389ms | -| 吞吐 | 达成率+错误率+δp99 | ≥99% @500rps | ≥95% | 499.9rps | -| 冷启动→MainWindowTitle 就绪 | N 次取 p50/p95 | ≤500ms | ≤1s | 171ms | -| Windows 桌面追加 | 全进程树 WorkingSet(含 msedgewebview2 子进程组)/WebView2 子组内存单列/全应用稳态 | ≤128MB † | — | 待测 | - -- †=暂定、门未激活;待 self-hosted runner(1C1G VPS 优先)实测钉绝对数。shared-runner 数字 warn-only,禁反推目标硬件。 -- Mode B SSE 缓冲豁免=**登记型豁免**(写入 PERF-BENCH「Known measured behavior」节 + upstream manifest 注记;解除条件=上游实现 SSE 特判 flush 即删)。 -- 适用域:不做竞品对标;Mode B 数据面性能属 Resin 领域;event gap 钉附加 delta 非绝对间隔(防测上游 WAN 抖动)。 -- SSE 行为面四断言(断连级联取消/有界背压/逐事件 flush/带内错误信令)并入验收。 - -## §2 票表(覆盖 D-003 / D-004 / D-005) - -| 票 | 档位 | 覆盖 | 内容 | -| --- | --- | --- | --- | -| **R12-00 验收线落地** | 恒 P0 | D-002 全部;D-003①②;D-005 修正① | bench 扩展:Windows 全进程树 WorkingSet(启动时后代树快照/job object 划界,禁进程名匹配——WebView2 共享运行时会跨应用串数)、好事件占比+达成率口径实现、SSE 四行为断言、Mode B 豁免登记写法、PERF-BENCH 表结构更新;冷启动门 N 次取 p50/p95(Defender/冷盘方差);**测量阶段与冒烟断言阶段失败语义分离**(测量数字永远是记录,冒烟失败才报)。**R12-02 并入本票**(三条件随迁:冒烟断言集≤5 条原样保留不稀释;失败只报不拦;macOS 缺口文档化;体量失控→同分支两 PR 系列而非两票);e2e remove 路径断言弱化(F-11)随冒烟集补回;ar/hi/th 字形不溢出截图断言顺路携带。产出物含「D-004 预算框架升格为验收线」的 ADR 草案。 | -| **R12-03 编排信号面补全** | P0 | D-003③ | 端到端出口探针接入 collect_verdict(bound port→经该 port 的 exit probe,慢调用/超时算失败)+ list_nodes failure_count 口径复核(窗口化/EWMA)。**硬约束**:SLI 探针为独立有界路径(每 tick 按 bound port 数封顶),不寄生 orchestration enabled 开关——编排控制器只是其消费者,关编排不得失明验收线。 | -| **R12-05 canvas write/read fault 救援** | 独立小票(0.5-1 天) | D-004 | 以 fix/zcode-GUI-Topology 的 4d9fa39e+255eb7b8 diff 作**规格说明书**(非补丁)在 main 重实现四不变量:patchSubscriptionsViaStrategyConfig 写 subscription intent+a_class:'subscription';onEdgesDelete 改 subscriptions 减法;边 id 加 viewMode 前缀强 remount;无条件边不带标签。+210 行测试整体移植并适配当前 API;commit body 注 provenance(recovered from fix/zcode-GUI-Topology @ 4d9fa39e, reimplemented for CanvasV4);**分支保留至合并验证后才删**。ADR 重编号:分支 0052→0075 直接入列(加「重编号自分支、2026-09-22 对 main 复核仍有效」注记);0050/0051 先 triage 对照 CanvasV4 现状,仍成立编 0076/0077、已吸收记 not-ported;CONTEXT.md 词条(Subscription Intent/无标签无条件边)随 0075 同 commit。禁机械 cherry-pick。 | -| **R12-06 Resin 定位评估** | 0.5-1 天 | D-003④ | 「产品真实形态是否=Resin 本身、壳层价值边界在哪」评估:resin/.git vendored fork 自带 Dockerfile/webui 的价值归属、mere-aggregation REST 缝纯洁性的结构性风险、README/验收线 §⑧ 适用理由复核。**输出为 ADR 草案,非重构授权**。 | -| **R12-01 React Query** | 劈半降档 | D-003⑤ | 0.5 天评估照做(Tauri invoke 双通道 queryFn 形态、SettingsView debounce 写入是否 mutation);迁移挂起,预写解封条件:(a) 评估发现具体正确性/性能收益;(b) 下一特性反正触碰三条缝。 | -| **R12-04 杂项扫尾** | 降档轮末 | D-003⑥⑦;D-005 触发线登记 | F-7..F-13 观察项打包(除 F-11 已移 R12-00)+ 残余叙事 token 清零裁决 + strategy_service 4000 行天花板 CI guard(仿 AGENTS.md size guard;触碰即拆口径维持)+ i18n 四触发线登记入册。 | - -**维持现状**(挂起/触发线制,全受 §3 巡检义务覆盖):TopologyView ⑤c churn 触发、tray i18n 手工表、Mode A 独立 crate deferred、url_encode_segment 触碰即换、converge 迁移挂起(见 R12-01)。 - -## §3 程序条款(覆盖 D-003⑨ / D-005) - -1. **runner 逾期 fallback**:self-hosted runner 注册逾期 4 周 → 用本机 Windows 剖面钉临时代表数,显式标注「非目标硬件」。 -2. **再过滤条款**:self-hosted 首跑回填后,§2 全部降档/挂起项重新过一遍验收线。 -3. **巡检义务**:每轮 grill 开工前必须产出锐评/债项完成度快照(本账本「锐评完成度快照」节为模板),触发线制挂起项全覆盖。 -4. **i18n 触发线登记册**(D-005):需求信号(≥3 独立非机器人 issue 指向同一非 en/zh locale)/采用证据(某非英语 locale 占比≥15%)/维护摩擦(tray.rs 年内≥2 次 locale 返工 或 i18n-check 失败≥1/月)/质量证伪(RTL/字形/溢出目验缺陷→干净退役:删目录+门控条目+tray 收缩)。AGENTS.md §3 定位声明已随本轮整理落地。 - -## §4 操作项(非票) - -- release-matrix dispatch 复跑 e2e 修复 6d035fdc(执行细节,随 R12-00 节奏)。 -- self-hosted runner 注册=用户侧动作;逾期自动走 §3-1 fallback。 - -## 执行顺序建议 - -R12-00 先行(SLO 铁律:先 SLI 管道后门)→ R12-03 / R12-05 / R12-06 可并行 → R12-01 评估 → R12-04 轮末扫尾。 - -## suggested skills - -| 任务 | skills | -| --- | --- | -| R12-00 / R12-03 / R12-05 实现 | implement(tdd 于约定缝)+ code-review 收尾 | -| R12-06 定位评估 / R12-01 评估 | research / atomcode-research(串行单发) | -| 全程版本控制 | gitbutler(写面唯一接口) | -| 代码取证 | codegraph 先行 + context-mode 路由(ctx_* 优先) | -| 下轮 grill | grill 系列(先读本账本 + 本任务书) | diff --git a/docs/agents/round12-wave-b-next-round.md b/docs/agents/round12-wave-b-next-round.md deleted file mode 100644 index 6d5e127b..00000000 --- a/docs/agents/round12-wave-b-next-round.md +++ /dev/null @@ -1,40 +0,0 @@ -# Round 12 Wave B 任务书(r12-wave-b grill 定稿产物,2026-09-22) - -> 唯一数据源:`.scratch/r12-wave-b-grill/decision-ledger.md`(D-001..D-003 全 current) -> 上游输入:`.scratch/r12-wave-a/handoffs/2026-09-22-audit-handoff.md`(审计残留 C1..C10)+ `docs/agents/round12-wave-a-next-round.md`(已执行完毕) -> 取证与调研:三轮 atomcode 深度调研(锚点见账本过程注记),零 revised。 - -## §0 轮次形态(覆盖 D-001) - -过滤轮:wave-a 审计移交的 10 项候选按四档规则逐项处置。C3 是唯一设计票(升 P0);C5 升档并入 bench 扩面;卫生项合并成一批;操作项不立票。 - -## §1 票表 - -| 票 | 档位 | 覆盖 | 内容 | -| --- | --- | --- | --- | -| **R12-B1 signal-plane 契约** | P0 设计票 | D-002 全部 | **顺序:ADR-0080 草案 → manifest/§7.5 → 实现**。①verdict 五值枚举 ok/local_fail/remote_fail/skipped/environment_suspect + detail≤64B(§7.5 校验,只进 IPC/audit 不进 ring);join 矩阵:loopback ok+egress fail=remote_fail、loopback fail+egress fail=local_fail、矛盾态记 detail 按 local_fail。②ring bool→ProbeVerdict;suspect tick 以枚举值入 ring 当时间屏障(评估器跳过、不进 Good-Event 分母);WindowStats 分母=valid 样本。③返回面加字段不删字段:signals:N 保留 + platform_verdicts 数组(platform/verdict/ok_share/streak,≤平台数有界);manifest --write 重生成。④共模抑制器:单 tick local_fail 占被探测 enabled bound-port 总数≥80%→suspect;ADR 写 n 退化表+单端口退化语义+引用 wave-a D-001 cooldown 兜底。⑤suspect-streak=tick 级 u32(独立平台 ring),连续 3 写死→audit row(tick 维度)+environment_status 字段。⑥ADR-0074 参数不动;ADR-0080 显式 supersede orchestration.rs majority-rule 段(互链 0074)。⑦同 commit:CONTEXT.md 词条(ProbeVerdict/Common-mode Suppressor)+codegraph sync+必测(join 矩阵、suspect 截断连续性)+UI 投影 skipped 渲染态。登记灰区:WAN 半残误归 remote_fail→detail 带 latency 观测值;解除条件=误切≥1 次/月→重开评估。 | -| **R12-B2 bench 扩面:VPS 全树** | P1 | D-001(C5) | headless 剖面全进程树 idle/startup 补测(Linux 侧 cgroup/进程后代划界,对齐 Windows 侧后代树快照口径);PERF-BENCH 表补行;验收线 §1 的 headless 行边界由此闭环。 | -| **R12-B3 webview-smoke 排查** | P0 | D-001(C1)+账本事实更新 | 首跑 35730121030 FAILURE(report-only):s2-nav-switch [data-testid=net-save-btn] 8s 未出现、s4-remove-path 点击路径失败(cardGone:true)、尾部 WebDriver execute/sync 级联。任务=分清选择器漂移 vs 真回归并修复;report-only 语义保持(失败只报不拦,D-003② wave-a 条款随迁)。 | -| **R12-B4 卫生批** | 轮末 | D-001(C4/C7/C9 登记) | 合并边界上限一批:C4 ipcLightweightSet debounced .catch 直修+测试(已文档化缺陷);C7 markdownlint 前置 pre-merge(verify-build.sh 挂载或分支触发 workflow——PERF-BENCH MD049 合并时才暴露的实证);C9 flake 观察已入册(实证≥2 次升修复票)。 | -| **操作项(非票)** | — | D-001(C2/C10) | self-hosted bench dispatch=用户侧动作(fallback 计时 2026-09-22 起算 4 周→本机钉临时代表数);本地 fix/zcode-GUI-Topology 分支 -D=wave-a D-004 兑现(重实现已合并验证,执行时按破坏操作规程确认)。 | - -**维持现状**(全在登记册,受巡检义务覆盖):⑤c、tray i18n、Mode A crate、url_encode、RQ 迁移、i18n 四线、bench † 项、NODE_FC(D-003 新入册)、strategy_service 预占线(新入册)、AGENTS.md 32KiB(余量 12B——本轮未动)。 - -## §2 程序条款 - -继承 wave-a 任务书 §3 全部条款(runner fallback/再过滤/每轮巡检义务/i18n 触发线登记册)。新增:登记册已同步本轮裁决(C8 verdict+触发线、C6 预占线、C9 观察、webview-smoke fired 状态)。 - -## §3 执行顺序建议 - -R12-B3(证据链断点先修)→ R12-B1(ADR 先行)→ R12-B2 → R12-B4 扫尾;操作项并行。 - -## suggested skills - -| 任务 | skills | -| --- | --- | -| R12-B1 设计票 | implement(ADR-0080 草案先行)+ code-review 收尾 | -| R12-B2 / R12-B3 | implement(tdd 于约定缝) | -| R12-B4 卫生批 | implement 小修集合 | -| 全程版本控制 | gitbutler(写面唯一接口) | -| 代码取证 | codegraph 先行 + context-mode 路由 | -| 下轮 grill | grill 系列(先读 wave-b 账本 + 本任务书 + 登记册) | diff --git a/docs/agents/round12-wave-c-next-round.md b/docs/agents/round12-wave-c-next-round.md deleted file mode 100644 index 2b5bbcde..00000000 --- a/docs/agents/round12-wave-c-next-round.md +++ /dev/null @@ -1,62 +0,0 @@ -# R12 Wave-C — 下一轮任务书(r12-wave-c-grill 定稿 2026-09-23) - -> 数据源(唯一):`.scratch/r12-wave-c-grill/decision-ledger.md`(D-001/D-002 全 current)。 -> 正本:`.scratch/r12-wave-c-grill/handoffs/next-round.md`;本文件为 git 镜像。 -> 前置已结:wave-b 五 lane 全 land 上 origin/main(tip 9d3b5ce7),合并态 ci/webview-smoke/Docs Governance 三绿;远端仅 main;无遗留票。 - -## 0. 轮次形态(D-001) - -**证据轮**。W3「environment_suspect 抑制器真实运行证据」升格主票 R12-C1;卫生批 R12-C2 随车;W4 发版 0.2.0 在证据落地后切(R12-C3,2 工作日解耦条款)。W2 self-hosted runner 继续既有 fallback 计时(2026-09-22+4wk→2026-10-20 逾期→本机钉临时代表数并标注非目标硬件);W8 产品方向裁决推迟至 wave-a D-003⑨ 再过滤条款执行时(C2 † 回填或 fallback 钉数后重排优先级)。 - -## 1. 票表 - -### R12-C1 — fault-inject 证据票(P0;覆盖 D-001+D-002) - -**目标**:给 ADR-0080 共模抑制器拿到「可信非单测」运行证据(工业惯例:防护机制上线≠可信,须有真实故障注入证据)。 - -规格(D-002 八格 + audit 选项 A): - -1. **载体**:`run-bench.mjs` 新增 `faultinject` 相位;`--phases faultinject` 按需启;不进默认相位集、不进 verify 门;bench.yml 经既有 phases dispatch 输入可选携带。 -2. **SUT**:headless exe(POST /api/v1/shell/orchestration/tick 手动驱动 tick,不等 60s cadence;抑制器在 resin-core 共享层双传输同码)。fixture 显式 `autonomy=suggest`——把 Auto 迁移隔离出断言面。 -3. **fixture**:spawn 前预置全新 stateRoot——`egressapikey.db` 端口行(5 平台 × 1 mixed enabled 端口;探针枚举源是 db `list_ports()` 非 whitebox json)+ `egressapikey-strategy.json`(orchestration enabled + platforms a_class=Region);每 run 全新 stateDir;直写文件绕 cascade 合法;REST PUT 仅运行时调整。 -4. **场景序列**:baseline ok(锚,失败重试一次)→ kill forwarder + 3 手动 tick(注入窗 <60s,避开 60s driver 插入)→ suspect 断言 → poll-wait 恢复(Resin 节点重启须过 cloudflare 健康探针才 routable)→ kill mock node + tick → remote_fail 断言 → 恢复 → clean tick → streak 归 0。 -5. **断言面**:tick 返回 `platform_verdicts` + `environment_status.suspect_streak`;streak 断言=单调不减 + crossing==3 恰一行环境事件 + clean 归 0(不写死 1→2→3——60s driver 可能插同判 tick)。suspect 段锁:平台不迁移、verdict 盖 environment_suspect;remote_fail 段锁:全平台 remote_fail、loopback ok、streak 不动、无环境事件。 -6. **audit(选项 A)**:同票补 `headless_main` 一行 `resin_core::audit::init(state_root.join(AUDIT_LOG_FILE))`——headless 未 init audit sink 系真实缺口补齐(与 headless 同 L2 存储叙事一致)。文件断言=streak==3 时 `audit.jsonl` 恰一行 `signal-plane/environment_suspect/orchestration:tick`(streak 4/5 不再发)。 -7. **注入载体重述(票内记 provenance 行)**:D-001 ①停 mock upstream→kill mock node(egress 探针硬编码 `1.1.1.1/cdn-cgi/trace` 经 entry port→sidecar→mock node→真网,mock upstream 不在探针路径);②bind-refuse→kill forwarder(Mode A 壳侧 entry 失效同一被测类,探针行为等价)。kill sidecar 原案码上不成立(loopback+egress 同死→join 产 local_fail 非 remote_fail)。同一 fail 类映射,非 revised。 -8. **降级与边界**:forwarder 缺失→相位记 skipped(复用 modeA skipped 语义)不 fail;faultinject 需真网出口(1.1.1.1)——mock.mjs「zero internet」头注释对本相位失效,票面+PERF-BENCH 写明;CI 网络抖动=固有 flake 源(缓解:slow_call_ms 调大+baseline 重试一次);Toxiproxy 留注释不进依赖;合成 local_fail 只证 loopback 全拒形态,WAN 半残灰区归 ADR-0080 豁免+soak 兜底不重复立法。 -9. **纪律**:无新 IPC 命令;选项 A 动 src-tauri 一行→CI verify 覆盖(ADR-0072);n=5 平台恰卡最低非平凡 n(⌈0.8·5⌉=4);断言不锁迁移终态。 - -验收:bench.yml dispatch `--phases faultinject` 双腿(windows+ubuntu)绿;注入证据行写入 release notes 素材。 - -### R12-C2 — 卫生批(覆盖 D-001) - -- W5 审计容忍级微疵:run-bench.mjs phaseHeadless/phaseApp 相位重复 ~90 行参数化、waitTcpReady/killTree 与既有内联实现去重、PlatformsView 两处 disjunct/命名拷贝。**排序条款:C1 先落、C2 后扫**——同触 run-bench.mjs 避免 rebase 撞车。 -- W7 cosmetic:`git fetch --prune` 清本地 stale remote-tracking refs(b1/b2/b4/docs-consolidation 残留)。 -- W1 登记册 webview-smoke→resolved:已在 wave-c 整理 commit 完成(合并态首跑 35755345313 双腿绿正面兑现);本票只复核无遗漏。 - -### R12-C3 — 发版 0.2.0(覆盖 D-001) - -时机:R12-C1 证据落地后切(release notes 带「抑制器已获注入证据」行);若 C1 超 2 工作日未落则先切、notes 如实标注 †暂定项与抑制器证据状态——不互相绑架。 -内容:版本位 bump(按既有发版流程对齐 Cargo.toml workspace / package.json / tauri.conf.json)、CHANGELOG [Unreleased]→[0.2.0] 收编、RELEASE_NOTES.md 更新(如实标注 † provisional + 抑制器证据状态)、tag + CI release matrix dispatch(workflow_dispatch)、产物 chunk-hash 核验。 - -## 2. 程序条款 - -- W2:self-hosted runner 注册仍用户侧动作;fallback 计时 2026-09-22+4wk→2026-10-20 逾期本机钉临时代表数并标注非目标硬件。 -- W8:产品方向裁决推迟至 C2 † 回填或 fallback 钉数后的再过滤(wave-a D-003⑨)。 -- W6:strategy_service 3636/4000 预占线 armed,不主动拆。 -- 若实现中产生新领域词:随实现 commit 入 CONTEXT.md(wave-b D-002⑦ 先例)。 -- AGENTS.md 12B 余量 armed:本轮任务书入 docs/agents/,不触 AGENTS.md。 - -## 3. 操作项(非票) - -- 登记册巡检义务照旧(wave-a D-003⑨):每轮 grill 复查各行 probe 列。 - -## suggested skills - -| 任务 | skills | -|---|---| -| R12-C1 实现 | implement + tdd(断言面先行);codegraph 取证已入票(探针枚举源/join/suppressor 行号) | -| R12-C2 | 卫生批直接 implement;同触 run-bench.mjs 注意 C1 先落 | -| R12-C3 | CI-only 纪律(ADR-0072)+ build-all 管道顺序参考;release matrix dispatch | -| 版本控制 | gitbutler(每票一 lane,C1 先落避免 C2 rebase) | -| 下轮 grill | grill-with-docs + 本账本 + trigger-line-register | diff --git a/docs/agents/round12-wave-d-next-round.md b/docs/agents/round12-wave-d-next-round.md deleted file mode 100644 index 4797496c..00000000 --- a/docs/agents/round12-wave-d-next-round.md +++ /dev/null @@ -1,117 +0,0 @@ -# R12 Wave-D Next Round — Task Book - -> Consolidated 2026-09-23 from `.scratch/r12-wave-d-grill/decision-ledger.md` (D-001..D-005, all `current`). -> Canonical copy: `docs/agents/round12-wave-d-next-round.md`; session artifact copy: `.scratch/r12-wave-d-grill/handoffs/next-round.md`. -> Ledger is the sole decision source. Do not re-derive dispositions from memory. - -## §0 Round shape (D-001) - -Wave-d = critique-adjudication round, complete. Output = four implementation tickets (R12-D1..D4) + register write-backs (applied at consolidation, see §5). - -Carried negatives (binding): - -- No armed/parked register line fires without new machine-checkable evidence; an external critique mentioning line counts is evidence logged, not a trigger. -- W8 product increment stays deferred (r12-wave-a D-003⑨ defer clause; bench-selfhosted fallback 2026-10-20 not reached). -- No unmeasured migrations: r2d2/deadpool, React Query, converge rewrite — measurement lines exist, tickets do not. -- resolve_id double-RTT / first-byte protocol detection / echo stubs are adjudicated (closed-observation verdicts, §5). -- If ANY ticket must touch `strategy_service.rs`, the feature-touch arm fires naturally — split ticket preempts. -- Pre-implementation gate: read ADR-0080 in full before R12-D3/R12-D4 land (legislated D-004/D-005). - -## §1 R12-D1 — X-Forwarded-Proto trust boundary (P1, security fast lane) [covers D-002] - -Surface: `src-tauri/src/headless_main.rs` `security_guard` unconditionally trusts `X-Forwarded-Proto` -> forgeable `; Secure` cookie suffix on plain-HTTP VPS profile. - -Spec: - -- Default: XFP fully ignored; `forwarded_https` true only when peer IP ∈ trusted set AND XFP first scheme == https. -- `--trusted-proxy <ip-or-cidr>` repeatable clap flag -> `Vec<IpNet>`; **`ipnet` declared as direct dependency** (lockfile-transitive does not excuse declaration). Exact IP covers loopback proxies; CIDR covers docker/k8s dynamic peer ranges. -- `axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())`; `security_guard` gains `ConnectInfo<SocketAddr>` extractor. -- Pure fn `trusted_forwarded_https(peer, trusted, xfp) -> bool`: IPv4-mapped-IPv6 normalization via `to_ipv4_mapped()` (NOT `to_ipv4` — maps `::1`->`0.0.0.1`); XFP list values take FIRST scheme (client-nearest, ASP.NET semantics), fail-closed. -- Forged XFP silently dropped (per-request warn = flood vector; optional `tracing::debug!`); startup info line prints trusted-proxy count. -- `0.0.0.0/0` = operator footgun: docs warning only, no hard block. -- **errata r12-wave-i D-002 (2026-09-26)**: the FIRST-segment rule and the `0.0.0.0/0` "warning only" posture in this ticket were superseded on landing - merged header instances take the RIGHTMOST (nearest-trusted-hop) segment, and wildcard CIDRs now refuse startup unless `--trusted-proxy-unrestricted` is given. Current truth: src-tauri/src/headless_security.rs + .scratch/r12-wave-i-grill decision ledger. -- ADR-0071: appended errata (amend-not-rewrite) recording why socket-peer model was chosen over XFF chain (guard only needs "did a trusted proxy relay this", not real client IP). Deployment doc gains the flag; CHANGELOG/RELEASE_NOTES behavior-change line in the SAME commit. -- Tests: 8 pure-fn cases — no flag ignores / untrusted peer ignores / trusted+https -> Secure / trusted+no-header -> none / XFP=http -> none / trusted+`https,http` list / v6-mapped `::ffff:127.0.0.1` hits v4 entry / CIDR hit+miss. - -Negatives: no deps beyond ipnet; desktop IPC/manifest/`session_cookie()` untouched; AGENTS.md zero delta (12 B headroom, armed line — flag docs go to `docs/`); XFP read moves near `plant_cookie` block so both reads stay colocated. - -## §2 R12-D2 — path-segment encoding consolidation (P2 correctness) [covers D-003] - -Surface: `ip_reputation.rs:137` builds IPQS URL `.../api/json/ip/{key}/{ip}` with `form_urlencoded::byte_serialize` — space->`+` is literal `+` inside a path segment -> auth fails on space-bearing keys. - -Spec (one ticket, TWO commits): - -- New module `crates/resin-core/src/encoding.rs` exports `encode_path_segment()` (strict set) + `encode_uri_component()` (JS-parity set, name retained — account-header-rule contract tests already prove that set). Name `urlencoding` is BANNED (its form-vs-path ambiguity caused this bug). -- commit-1 (minimal fix): ip_reputation -> `encode_path_segment` + IPQS URL-construction regression assertions. -- commit-2 (byte-equivalent mechanical migration): the three named path-segment sites (resin_client `urlencoding`, headless_main `url_encode_segment`) -> `encode_path_segment` + `encode_uri_component` moved into the module. Query-value call sites KEEP the strict set (over-encoding always safe; byte change deferred to a real caller). -- Register discharge wording: "consolidation positively discharged (touch-rule fired on the three named sites)" — discharge hangs on commit-2. -- Tests: space->%20, +->%2B, /->%2F, non-ASCII UTF-8, IPQS URL regression, `encode_path_segment`->headless `percent_decode` round-trip. - -Negatives: zero new deps; AbuseIpDb `parse_with_params` / raw `Key` header / `percent_decode` untouched; zero CONTEXT terms; zero AGENTS.md delta; do not assert IPQS server `%20` tolerance without credentials (worst case not worse than today). - -## §3 R12-D3 — audit-chain robustness (narrowed) [covers D-004] - -Surface: `crates/resin-core/src/audit.rs`. Two critique claims disproven at code read (field-order fragility — chain hashes STORED bytes not reserialized events; `.tmp` residue — none exists; `written_bytes` already seeds from `metadata.len()` at open). Real residue below. - -Spec: - -- Tail adoption: `last_line_hash` candidate check `from_str::<Value>` -> `from_str::<AuditEvent>` (valid-JSON-but-not-an-event no longer becomes chain tail). -- Optional single-link back-verify warn: adopted tail's `prev_hash` recomputed against the preceding complete event row; mismatch -> still adopt (forward continuity) + `tracing::warn!` (ADR-0059 D7: warn never blocks). -- `written_bytes`: re-seed from `metadata.len()` after EVERY append; on stat failure keep old value (no zero, no error). Rotation sets 0 -> next append re-seeds (self-consistent). -- ADR-0059 errata CORRECTS D3 wording (it currently teaches verifiers wrong): (1) hash = sha256(stored line bytes + `\n`), verifiers forbidden from reserializing; (2) torn tail bytes preserved, never truncated; (3) mid-stream garbage tolerance contract — verifier MUST accept "garbage line mid-stream, chain links over it"; (4) backward-walk skip semantics. Plus one-line disclosure: directory-entry fsync absent after create (best-effort per D7, file could vanish not torn). -- Module doc: single-writer invariant — `append` reads `last_hash` and writes back across the I/O in two critical sections; safe today under the process-global `AUDIT` Mutex; before any future multi-writer wiring the prev_hash read-modify-write must move into one critical section (documented, NOT a ticket). -- Tests: tail injection x4 (valid event / valid-JSON-non-event / torn / empty) + `AuditLog::new` counter == metadata.len() + forensic-preservation assert (garbage bytes still present AND new row prev_hash == last complete event hash) + back-verify warn path. - -Negatives: no marker rows; no serialization change; no streaming/incremental writes; no offset file; audit.rs + ADR-0059 errata are the only touched surfaces; zero IPC/manifest/AGENTS/CONTEXT. - -## §4 R12-D4 — closing batch: hygiene + faultinject monthly + dev counter [covers D-005] - -### 4a Hygiene items - -- `PlatformsView.tsx` A||A dead disjuncts :554/:589/:590 — pure deletion; verified :590 `protocol_mismatch` lives in the else chain and stays reachable. Ticket keeps an else-chain reachability check with ceiling = no ternary refactor. -- `" selected"` hardcode (:784) -> i18n key added to ALL 18 locale dirs. -- `manualSubExpanded`/`setManualSubCollapsed` name-value mismatch (:120) — rename only. -- `platformSubName`/`platformGroupBySub` (:65/:68) — dedupe against the NodesView pattern. -- Audit obs 1: `run-bench.mjs:687-694` assertion area — pin `streak>=3` as named constant + comment. Zero file overlap with R12-D3; must NOT touch `:75` PHASES registry. -- Audit obs 5: `mock.mjs:15-16` stale "only loopback permitted" comment -> truthful comment citing the R12-C1 external-CONNECT exception. -- Audit obs 2: `waitAllClosed` dropped return (:680) -> trace only, never error/fail. -- Exemptions recorded IN the ticket file (prevents critique re-surfacing): obs 3 (Toxiproxy comment absence), obs 4 (BENCH_FWD_* env dup), obs 6 (third bare unwrap) — tolerated per audit recommendation. - -### 4b faultinject monthly schedule (legislation) - -- bench.yml gains second cron `0 3 1 * *`; schedules cannot pass inputs, so: - - `PHASES_ARG: ${{ github.event_name == 'schedule' && '--phases <explicit full default set>,faultinject' || (inputs.phases != '' && format('--phases {0}', inputs.phases) || '') }}` — verbatim into the ticket; phase list resolved from `run-bench.mjs:75` PHASES default at implementation. EXACT semantics: explicit full list + faultinject (harness additive semantics REJECTED — would violate the phase-registry freeze). - - Build-step `if:` at :99/:107 gain `|| github.event_name == 'schedule'`; windows leg `with_app` is empty under schedule — same treatment. - - Ticket carries a dry-run verification step for schedule-path behavior. -- Self-referential register line (applied §5): monthly run absent >=2 consecutive occurrences -> review (GitHub 60-day auto-disable). -- bench-selfhosted.yml stays dispatch-only forever — SECURITY RAIL reaffirmation, not new law. -- faultinject numbers NEVER flow into acceptance.json pin columns (suppressor survival evidence, not perf baseline). - -### 4c converge measure-first counter - -- Permanent dev-only counter: monotonic-timestamp marks at both `ipcAuthoritativeSnapshot` trigger-path entries; duplicate criterion = in-flight overlap ONLY (second mark lands while first invoke unresolved; sequential triggers explicitly not counted — ADR-0069 D3 drift-visibility design). `import.meta.env.DEV` compile-time elimination, ~10 lines. -- Positive: >=1 measured in-flight overlap -> React Query arm (a) fires naturally. Negative closure: two observation cycles (two release cycles) at zero -> register records "measurement complete, negative, stays SUSPENDED". - -Negatives: pure hygiene — no behavior contract changes; schedule branch never enters default phase set or verify gate; the three file-touch surfaces must not trip register touch-rules; do not touch `run-bench.mjs` phase registry (R12-C1 acceptance context). - -## §5 Register write-backs (APPLIED at consolidation commit) [D-005] - -New armed rows: port-scan (measured suggestion-latency or >=1 allocation-conflict -> ticket); DbPool lock-wait (tied to bench-selfhosted measurement; CANNOT FIRE while runner unprovisioned, fallback 2026-10-20); backup-envelope (any envelope change need -> open); ubuntu s1 waitForTitle (baseline 35755345313 both legs green; recurrence = same s1 step fails AFTER merged-state green); faultinject-60day heartbeat. - -Annotations: strategy_service row notes critique as independent external evidence (headroom refreshed 3636/4000=364, thresholds unchanged); i18n line 2 dead-arm note (no telemetry -> permanently unevaluable); React Query SUSPENDED -> measuring (armed by 4c counter). - -Closed-observation verdicts: resolve_id double-RTT (ADR-0071 D-002-2 Accepted); dead-semantics manifest commands account_add/account_bind_ip (AGENTS §7.6 + CONTEXT `Echo Command`); i18n-check Rust-side blind spot (observation, no line). - -## §6 Ordering & ops - -- D1/D2/D3/D4 file sets are mutually disjoint (headless_main / encoding+ip_reputation / audit.rs / frontend+bench) — parallel-safe lanes; one commit each (D2 = two commits by spec). -- Ops: first monthly faultinject scheduled run needs a watched first execution (dry-run step in ticket). bench-selfhosted runner registration remains a user-side action (fallback 2026-10-20). - -## Suggested skills - -- `grill-with-docs` — next adjudication round. -- `atomcode-research` — any research request (serial, one run in flight). -- `gitbutler` (`but`) — all commits/pushes/landing. -- `domain-modeling` — only if a genuinely new domain term appears during implementation (none legislated this round). -- `neat-freak` — post-landing closeout. -- `sqlite-utils` — if touching `egressapikey.db` fixture rows in tests. diff --git a/docs/agents/round12-wave-e-next-round.md b/docs/agents/round12-wave-e-next-round.md deleted file mode 100644 index 556f3678..00000000 --- a/docs/agents/round12-wave-e-next-round.md +++ /dev/null @@ -1,92 +0,0 @@ -# R12 wave-e — next-round task book - -Canonical: `docs/agents/round12-wave-e-next-round.md` -Handoff copy: `.scratch/r12-wave-e-grill/handoffs/next-round.md` -Sole spec source: `.scratch/r12-wave-e-grill/decision-ledger.md` (D-001..D-005, all current) -Produced: 2026-09-24 wave-e grill consolidation. Every task declares its D-xxx coverage. - -## 0. Round shape (D-001) - -Agenda C = adjudication round + observation write-backs merged. Main ticket: critique #2 Mode A dual-kernel adjudication (the last unadjudicated architecture charge of the second critique). Side tickets: macos-x86_64 delivery-surface verdict + DbPool dead-arm rewrite. Small ticket: residual-minor pair inside the closing batch. Observation items land as register write-backs / ticket notes, never as tickets. - -Binding negatives carried by the round: - -- No source edits during grilling; all five items are adjudicated specs — implementation is future work. -- Method: per-question code evidence + atomcode terminal adjudication; zero revised records. -- The s1 armed line FIRED this round on machine evidence (runs below) — its adjudicated remedy joins the batch, it is not an open question. - -## Fixed points - -- Branch `ca-branch-1` tip `482657a6` (wave-d impl + audit fixup); verify `35947178042` green; release-matrix `35882970803` -> draft `r12-waved-ci.1` (6 assets, no x86_64 dmg). -- s1-window-up FAIL signature `documentTitle:"", nativeTitle:null`: `35839072568` (2026-09-23, first - not counted) + `35948350717` (2026-09-24, recurrence -> line fires). -- macos-13 runner image RETIRED 2025-12-04/08 (GitHub changelog); `macos-15-intel` is the last Intel image, retires 2027-08. -- `port_forwarder.rs` 1413 lines (Mode A legislated dataplane); `db.rs` 614 lines; `e2e-webview/smoke.mjs:131` one-shot `getTitle()` = the only non-polled assertion in the script; `ip_reputation.rs:30` `parse()` = 7-line alias fn, no direct test. -- parking_lot `Mutex` has no wait-time API; eventual-fairness forcing ~= 1 ms (0.5 ms avg) — the dev-counter threshold anchor. -- r2d2 is 404-dead on crates.io (ADR-0011 note, 2026-08-03) — pool successor = `deadpool-sqlite`. -- ADR-0068 D4: TLS fronting explicitly out of scope (cite verbatim, do not re-legislate); D2 = protocol-family freeze origin. -- AGENTS.md 32 KiB ceiling armed: any net-addition trips the verify gate; wave-e doc edits must be net-negative or externalized. -- Research artifacts: `.codex-tmp/atomcode-wavee-q{2,3,4,5}-prompt.txt` + `.codex-tmp/atomcode-wavee-q{2,3,4,5}-report.md`. - -## R12-E1 — Mode A boundary-law registration + CI assertions (D-002, main ticket) - -Outcome: Mode A is NOT a second-kernel violation — ADR-0068 legislated it. Register existing law + boundary law; this is NOT a new amnesty (wording discipline so future readers do not mistake Mode A for drift). - -Deliverables: - -1. Handler census of `port_forwarder.rs` -> frozen inventory of handlers/rewrite rules/StreamSensor dimensions = the CI assertion baseline. -2. `mode-a-contract` gate scenario-matrix assertions for the three bans: protocol-family freeze (rewrite whitelist = {socks5-handshake, http-connect, absolute-form->CONNECT}); no TLS fronting (cite ADR-0068 D4 verbatim); no new L7 features (new dataplane needs go to the fork line). -3. Register row landed in the wave-e closeout: armed line, 4+1 triggers — (a) VPS credential-free demand; (b) Mode B checkpoint defect outside Registered Exemption; (c) net-new dataplane behavior (new handshake dialect / new rewrite rule / new StreamSensor dimension; line count = warn-only reference, not the trigger); (d) Resin ships a StreamSensor-usable tap endpoint -> Mode A decommission auto-starts; (e) collected ADR-0079 orphan line (upstream ships native port-identity -> re-open). -4. Acceptance criteria MUST carry the reverse pointer to R12-E4: boundary-law CI assertions are delivered HERE; E4 register write-back only references them (mutual pointers, no orphaned assertions). -5. Closed record landed in closeout: edge-adapter-as-separate-process evaluated and rejected. - -Negatives: no upstreaming now; no crate extraction; no thin-shell regression (revoking ACCEPTED ADR-0068 is out of ticket authority); StreamSensor stays shell-side; Mode B default untouched. - -## R12-E2 — macos-x86_64 input-gate + macos-15-intel migration (D-003) - -Deliverables: - -1. `.github/workflows/ci.yml` release-track jobs (backend `:119` + gui `:214`): `os: macos-13` -> `macos-15-intel`, plus a job-level `if:` on a dispatch input `inputs.include_intel_mac` (default off; non-Intel legs always build). -2. Raise per-job `timeout-minutes` (Intel-runner builds are empirically slow). -3. Docs — RELEASE_NOTES + README download table + AGENTS.md artifact list: "macOS = arm64 (Apple Silicon) first-class; x86_64 = opt-in best-effort until 2027-08 (macOS 15+ Intel only)". AGENTS.md edit must be net-negative. -4. ADR-0010: append a Revision-section note (precedent exists) — no new ADR. -5. Verify the draft-release collect step does not hard-assert the x86_64 dmg exists. -6. Register rows landed in closeout: hard-date 2027-08-01 line + >=3 non-bot-issue demand line + universal2 succession note (arm64 runner + x86_64 target + lipo; designated successor, NOT implemented now). - -Negatives: delete the impossible signal (GitHub restoring free Intel capacity); no paid larger runner; no doc-only change; not A1 today (kills a real escape hatch early). - -## R12-E3 — DbPool dead-arm rewrite: dev counter + synthetic probe (D-004) - -Deliverables: - -1. dev-only lock-wait counter on DbPool acquisition: `Instant`-wrapped, >=1ms threshold (anchored to parking_lot eventual-fairness forcing line), DEV-gated / zero-cost in prod (convergeDevMark pattern). -2. Synthetic concurrency probe: cargo test, 8 threads hammering `list_ports` x N rounds, measure wait p99 — upper-bound proof, CI-runnable, zero runner dependency. -3. Register rewrite landed in closeout: four arms (i) dev >=1ms real load -> reopen (successor deadpool-sqlite; r2d2 404-dead recorded); (ii) probe p99 >=1ms -> same; (iii) dev counter zero for two cycles AND probe p99 <1ms -> closed verdict; (iv) hard date 2026-10-20 verdict MUST land regardless of runner. -4. Recorded observation landed in closeout: read-path spawn_blocking asymmetry (write path already exempt per ADR-0011; tokio blesses short holds — no trigger line). - -Negatives: no RwLock / FairMutex / hand-rolled mini-pool; no r2d2; no lock-architecture change. The probe is an existence-of-contention instrument; the selfhosted `dagger` line still owns absolute-latency thresholds. - -## R12-E4 — closing batch (D-005) - -1. s1 fix — `e2e-webview/smoke.mjs` s1 block ONLY: wrap `getTitle` in `waitUntil` (or a poll matching the repo `waitPort` style): title === "EgressAPIKEY" or 30s timeout, 500ms interval. Non-masking conditions verbatim in the ticket/PR: (a) same criterion, not relaxed; (b) bounded timeout still FAILs and `detail` records the last observed value plus the title evolution across polls; (c) do NOT widen S2-S5 or waitPort/nativeTitle waits. Rejected remedies: workflow-level retry, longer timeouts. -2. `ReputationProvider::parse` table test (`ip_reputation.rs` mod tests): all aliases for the three providers, mixed case (e.g. "IPQS"), surrounding whitespace trim, unknown -> None. AGENTS section-4 compliance debt. -3. `subName` private — record only, no ticket. -4. Register write-back package — EXECUTED in the wave-e closeout commit (7 items): s1 fired + remedy wording; Mode A registered-legislation row; DbPool four-arm rewrite (old runner-gated text fully replaced); macos hard-date + demand + universal2 note; spawn_blocking observation; edge-adapter closed verdict; observation notes (convergeDevMark, faultinject first-run dry-run echo check, D3 warn = data, W8 user-side decoupled). Ticket file only references them. -5. Boundary discipline: Mode A CI assertions are E1-owned — E4 acceptance names the mutual pointer. -6. `codegraph sync .` in the same session as the parse-test commit. - -## Ordering & operating notes - -- E1..E4 are file-disjoint: E1 = port_forwarder / mode-a-contract gate; E2 = ci.yml + docs; E3 = db.rs + dev-counter wiring; E4 = smoke.mjs + ip_reputation.rs. Register write-backs are already committed — no register write conflicts between tickets. -- CI-only policy (ADR-0072): no local build/test; acceptance = the CI verify job. -- Grill closeout suggestion (procedural, not a ledger decision): read `docs/adr/0080` in full before landing E1/E3 — wave-d precedent. -- After source changes: `codegraph sync .` (recommended, not a gate). -- Version control: GitButler `but`; `.scratch/` is gitignored — session artifacts never commit. - -## Suggested skills - -- `implement` / `tdd` — E1-E4 implementation tickets (test-first where behavior changes). -- `code-review` — pre-merge two-axis review (the wave-d audit pattern). -- `atomcode-research` — any further external adjudication (serial only). -- `gitbutler` (`but`) — all commits/pushes. -- `neat-freak` — closeout after landing. -- `handoff` — next round handoff document. diff --git a/docs/agents/round12-wave-f-next-round.md b/docs/agents/round12-wave-f-next-round.md deleted file mode 100644 index 60808c6e..00000000 --- a/docs/agents/round12-wave-f-next-round.md +++ /dev/null @@ -1,70 +0,0 @@ -# R12 wave-f — next-round task book - -Canonical: `docs/agents/round12-wave-f-next-round.md` -Handoff copy: `.scratch/r12-wave-f-grill/handoffs/next-round.md` -Sole spec source: `.scratch/r12-wave-f-grill/decision-ledger.md` (D-001..D-003, all current) -Produced: 2026-09-24 wave-f grill consolidation. Every task declares its D-xxx coverage. - -## 0. Round shape (D-001) - -Agenda A = fired-line adjudication round. Main item: DbPool pool-impl adjudication (register arm (ii) fired on machine evidence). Small ticket: Mode A census extractor hardening + register stale measure. Observation items land as write-back notes only — never tickets. Armed items stay parked absent new evidence (no re-grilling). - -Binding negatives carried by the round: - -- No source edits during grilling; implementation is future work. -- Method: per-question code evidence + atomcode terminal adjudication; zero revised records. -- Do not reopen parked items (#1 monolith / #4 i18n / #7 envelope) without new evidence — trigger-line discipline. - -## Fixed points - -- Integration tip `ae9ecb29` = tag `r12-wavee-ci.1`; release-matrix run 35960121325 produced all 13 artifacts incl. both opt-in Intel legs; audit verdict PASS (`.scratch/r12-wave-e-grill/reports/2026-09-24-audit.md`). -- DbPool arm (ii) FIRED verbatim: `p99=9615us over 200 samples (max=9879us over_threshold=76)` on run 35959139983; fixup `fd2f1ad1` restored green (probe now reports + 250ms hang bound). -- `crates/resin-core/src/db.rs` 821 lines: `acquire()` single seam at :119 (debug_assertions-gated `LockWaitStats` instrument); probe test `concurrent_list_ports_lock_wait_probe_reports_p99` at :759. -- `scripts/mode-a-contract-check.cjs` 498 lines; fn extractor regex at :275 covers `pub?`+`async?`+`fn` only — misses `unsafe`/`const`/`extern`/macro-generated fns; current 39-fns census has no misses today (audit independent re-extraction). -- `tests/fixtures/mode-a-boundary/census.json` = frozen inventory; README documents the deliberate-change path (update census in the same commit + cite D-id/ADR). -- AGENTS.md actual size 32690/32768 B (78 B headroom) — register now carries the corrected figure. -- Research artifacts: `.codex-tmp/atomcode-wavef-q{2,3}-{prompt,report}.md`. - -## R12-F1 — census extractor hardening + instrument annotation alignment (D-003 + D-002 tail) - -Files: `scripts/mode-a-contract-check.cjs`, `tests/fixtures/mode-a-boundary/census.json`, `crates/resin-core/src/db.rs` (comments only), `docs/agents/trigger-line-register.md` (closeout already landed). - -Deliverables: - -1. Widen the fn-extraction regex (script :275) to the full qualifier space — `pub(…)?` + any combination/order of `const` / `async` / `unsafe` / `extern "…"` before `fn`; `extern "C"` quoted-string handled; invalid combos may over-report (fail-closed is intended). -2. Add `extractor_version` field to census.json — a regex upgrade must bump it and update census in the same commit (assertion self-documents). -3. Add the declarative-macro fail-closed assertion: any `macro_rules!`/`macro` definition in port_forwarder.rs -> gate FAIL with the offending line number and two disposal options in the message (inline the expansion into census / rewrite as a plain fn) — a ten-second decision, not a research problem. -4. Unify the `frozen_at` line-count convention to `content.split('\n').length` and add `line_count_convention` note to census.json. -5. D-002 tail — annotation-only alignment in `db.rs`: probe/instrument comments updated to record the adjudicated outcome (arm (ii) discharged with a keep-Mutex verdict; probe is now a regression/A-B instrument, not a production criterion; arm (i) is the primary instrument). NO code behavior change — debug_assertions gating, counters, and the 250ms hang bound all stay exactly as-is. -6. Verify the gate still passes 19/19 locally-as-script (the check runs in verify-build.sh — CI is the acceptance surface per ADR-0072). - -Rejected by adjudication (do not implement): - -- Trait-impl / proc-macro-call-site coverage expansion — impl-block fns are internal detail, not the dataplane contract (golden-file principle: freeze consumer-visible contract only); proc-macro deps are caught by Cargo.toml diff anyway. -- syn / rustdoc-JSON AST extraction now — nightly coupling + format drift (cargo-public-api #858, rust #135600) violates ADR-0072 minimal CI surface; the AST upgrade trigger line is armed in the register instead. - -## Register write-backs — LANDED in the wave-f closeout commit - -- DbPool row rewritten: arm (ii) consumed (fired -> keep-Mutex verdict, emschwartz/abseil cited); arm (i) = primary instrument; arm (iii) probe leg dropped (two zero dev cycles alone discharge); arm (iv) hard date 2026-10-20 unchanged; successor deadpool-sqlite arm(i)-gated with B-first ordering. -- New parked row: DbPool read-path unlock (P3) — fires only on measured headless/VPS read latency (never synthetic); remedy = long-lived LazyLock read-only Connection (hynek -shm warning recorded; snapshot_db_readonly precedent not transferable). -- New armed row: census extractor AST upgrade trigger (proc-macro dep / 3+ files / >=2 regex fixes per year). -- AGENTS ceiling measure corrected 32756 -> 32690 (78 B headroom); .scratch historical files intentionally untouched (point-in-time records). -- Mode A row status: extractor-hardening note appended (traceability chain D-002 -> wave-f D-003). -- spawn_blocking observation closed as verdict (harmless exposure). -- New R12-F observations section: s1 post-land pending, probe sanity assert accepted, rustc cfg-harness disclosed deviation, wave-e lanes awaiting user land. - -## Carried observations (no ticket) - -- s1 post-land: first nightly webview-smoke on main (cron 03:30 UTC) after the lanes land must show `s1-window-up` green + `titleEvolution` in detail; a repeat failure re-adjudicates the remedy. -- faultinject first scheduled run 2026-10-01 03:00 UTC — check the dry-run echo step for PHASES_ARG/HEADLESS_ARG. -- convergeDevMark overlap counter: two zero cycles -> negative verdict; >=1 overlap fires React-Query arm (a). -- W8 bench-selfhosted runner: user-side action; `dagger` provisional-thresholds line still depends on it (fallback 2026-10-20). -- If arm (i) ever fires on real load: remedy order is B (long-lived read conn, ~0.5-1 day) before A (deadpool interact()-migration, ~2-4 days) — recorded in the register row. - -## Suggested skills - -- `implement` / `tdd` — R12-F1 (script assertions + metadata; comment-only Rust diff). -- `code-review` — pre-merge two-axis review. -- `atomcode-research` — external adjudication only, serial. -- `gitbutler` (`but`) — all commits/pushes. -- `neat-freak` / `handoff` — post-land closeout. diff --git a/docs/agents/round12-wave-g-next-round.md b/docs/agents/round12-wave-g-next-round.md deleted file mode 100644 index 9ef1e87f..00000000 --- a/docs/agents/round12-wave-g-next-round.md +++ /dev/null @@ -1,86 +0,0 @@ -# R12 Wave-G Next Round - Task Book - -Source of truth: `.scratch/r12-wave-g-grill/decision-ledger.md` (1 current -record: D-001). Base: origin/main `e71cb54d` - wave-e + wave-f fully landed, -CI verify run 36033301265 green. - -Wave-g was a **verification/semantics round**: it produced NO code ticket. -This book is an operational duty list; every item cites its covering D-xxx. - -## 0. Round shape and binding negatives (D-001) - -- One discharge-hold (s1), one semantics legislation (convergeDevMark - cycles), one metadata hygiene (residual_limits), one roll-call. -- Binding negatives (ledger verbatim): - - NO re-grilling of parked/armed items absent new evidence (monolith, - i18n trim, envelope versioning, AST upgrade, edge-adapter, React Query - arm a). - - residual_limits is metadata-only: no gate judgment-path or census - change; zero runtime behavior change. - - The s1 waitUntil 30s/500ms poll is the remedy itself - permanent, - never weakened or removed. - - The s1 holding window counts SCHEDULED (cron) runs only - a cancelled - scheduled run does not count (35976721262, 2026-09-24). - - .scratch historical files are point-in-time: never edited. - -## R12-G1 - s1 scheduled-run watch + closure (covers D-001.1) - -State at handoff: EVIDENCE-POSITIVE, HOLDING. Push-run 36033301454 (main @ -e71cb54d) shows s1-window-up PASS on both legs with `titleEvolution` in -detail; the ubuntu leg captured the diagnosed race verbatim ("" at 11ms -> -"EgressAPIKEY" at 579ms) - mechanism-level proof the poll absorbs it. - -Duty: - -1. Watch the next scheduled nightly run(s) - cron 03:30 UTC, main only - (first candidate: 2026-09-26 03:30 UTC). -2. Count only SCHEDULED greens toward the 1-2 run window; cancelled or - push-triggered runs do not count. -3. On 1-2 consecutive scheduled greens -> update the register s1 row to - discharged (cite run ids) and move the observation to closed verdicts. -4. On ANY s1-window-up failure -> re-adjudicate the REMEDY (in-probe - polling), not the ticket; escalate back to grill with the new - titleEvolution evidence. - -## R12-G2 - convergeDevMark first real read (covers D-001.2) - -Duty: in the next real dev session (GUI run), read the dev-only overlap -counter (console tag `[converge]`); record the reading + UTC timestamp as -a register data point. Semantics legislated: an observation cycle = a REAL -dev-session read; unread cycles are no-data, NOT zero - never-observed can -never count as a zero reading. Hard date 2026-10-20 (shared backstop with -DbPool arm iv): still no real read by then -> record dead/unevaluable -(i18n line-2 dead-arm precedent). >=1 measured overlap fires React-Query -arm (a). - -## R12-G3 - scheduled-observation roll-call (covers D-001.4) - -- 2026-10-01 03:00 UTC: first monthly faultinject run (bench.yml cron - `0 3 1 * *` verified) - check the dry-run echo resolves PHASES_ARG / - HEADLESS_ARG; two consecutive absences fire the heartbeat line. -- 2026-10-20: DbPool arm (iv) hard date - verdict MUST land; no arm (i) - real-load corroboration -> closed-verdict archive. -- W8 bench-selfhosted runner: user-side registration; dagger - provisional-thresholds fallback shares the 2026-10-20 date. -- monthly faultinject heartbeat + D3 back-verify warn: carried unchanged. - -## Already landed in the wave-g consolidation commit (no action) - -- census.json `residual_limits` field (D-001.3): the two extractor blind - spots (same-line #[attr] fn; glued extern { fn }) enumerated; 0 - occurrences grep-verified; hand-verify mandate if ever introduced. -- Register write-backs: s1 row evidence-positive/holding; React-Query + - convergeDevMark cycle semantics; AST row cross-reference to - residual_limits; R12-G observations section; wave-e/wave-f lanes-landed - closure. -- CONTEXT.md Trigger Line entry refined: observation-cycle semantics - (NOT_OBSERVED != 0; hard-date dead/unevaluable exit). - -## Suggested skills for the next agent - -- `grill-with-docs` - next round's interview loop -- `gitbutler` (but) - any commits/pushes -- `handoff` - round closeout -- `atomcode-research` - evidence gathering per question (serial, one in - flight) -- `code-review` - only if new diffs appear diff --git a/docs/agents/round12-wave-h-next-round.md b/docs/agents/round12-wave-h-next-round.md deleted file mode 100644 index e8ebdbd4..00000000 --- a/docs/agents/round12-wave-h-next-round.md +++ /dev/null @@ -1,57 +0,0 @@ -# Round 12 Wave-H — Next-Round Taskbook - -Slug: `r12-wave-h-grill` | Baseline: `origin/main` at closeout (was 5a146f1d at grill start) -Ledger: `.scratch/r12-wave-h-grill/decision-ledger.md` — D-001..D-005, all current. -Reports: `.codex-tmp/atomcode-waveh-q2..q5-{prompt,report}.md` - -## How to use this taskbook - -- Each task declares the D-xxx IDs it covers. The ledger is the data source of record; this file operationalizes it. -- Register write-backs for D-004/D-005 land in the same closeout commit as this file — code/config tickets below are next-round work. -- Suggested skills: `$atomcode-research` for any new adjudication; `$gitbutler` for all version-control writes; `grill-with-docs / domain-modeling / neat-freak / handoff` for the next re-grill cycle. - -## R12-H1 — Resin upstream-first delivery package (D-002, D-003) - -Produce locally, in the ignored `resin/` clone (Go 1.26.2 on host; go.mod wants 1.25.5): - -1. Patch `resin/internal/proxy/forward.go` (~L294): replace the bare `io.Copy(w, resp.Body)` with an SSE-aware conditional flush writer — on `Content-Type: text/event-stream`, write then `Flush()` via `http.Flusher` assertion; non-SSE path byte-identical. Commit message: `forward: flush SSE responses per event`. -2. Tests in upstream's existing `resin/internal/proxy/forward_http_copy_test.go`: per-event arrival assertion + non-SSE invariance assertion. Verify: `go build ./...` and `go test ./internal/proxy/`. -3. Artifacts doc `docs/agents/wave-h-upstream-package.md`: - - PR title + body: symptom (SSE buffering 2-4KB windows) -> root cause at `forward.go:294` -> fix shape -> test evidence -> AI-disclosure line ("drafted with AI assistance; verified and defensible by submitter"). - - Probe-issue draft in Chinese, ending with a question such as "如欢迎愿发 PR?". - - User checklist: post probe issue -> await maintainer response -> rejection or stall >1 upstream release cycle (monthly cadence) -> dormant patch-fork trigger per D-002. - -Negatives: do NOT create the fork repo, open the issue, or open the PR — external actions under the user's GitHub identity are user-side (D-003.4). Do not pre-build Day-1 fork infra before the trigger fires. The dormant-fork Day-1 archive (fresh repo, main rebases upstream tags, copied release.yml, manifest repoint, THIRD_PARTY.md line) lives in D-002 and activates only on trigger. - -## R12-H2 — DbPool pool-impl evaluation ticket (D-004) [only code-touching item] - -- step0: extend the existing dev-gated `db.rs` lock-wait WARN with holder identity (one-line increment). -- step1: B-read-conn prototype — a long-lived `LazyLock` read-only `Connection` serving `list_ports`/`get_port` (hynek caveat: MUST be long-lived; short-lived readers hit SQLITE_BUSY on open/close) + A/B measurement in a real dev session. -- step2: if contention persists -> formal `deadpool-sqlite` evaluation. -- Optional (not a prerequisite): `db-lock-metrics` feature-gate variant for release-domain corroboration of arm(iii). - -Negatives: evaluation only — no migration commitment; do not retroactively rewrite arm text; arm(iii)/(iv) unchanged. - -## R12-H3 — pnpm configuration repair (D-005.2) - -- Resolve `pnpm-workspace.yaml` `allowBuilds` placeholders -> `{esbuild: true, edgedriver: true, geckodriver: true}` (verify with `pnpm why edgedriver geckodriver`; `false` is also legitimate if CI never runs the webdriver smoke — decide at implementation). -- Delete `package.json` `pnpm.onlyBuiltDependencies` — dead under pnpm 11 (package.json pnpm field unread). -- yaml + lockfile in one commit. NEVER `dangerouslyAllowAllBuilds`. -- First implementation step: align pnpm versions host vs CI (`pnpm --version` on both; the CI-green/host-red divergence is likely CI on pnpm10 silently skipping v11 config). -- Verify: `pnpm install` + relevant build/test on host. - -## R12-H4 — observation roll-call (carried; no new decisions) - -- s1 holding window: count scheduled cron greens only (cancelled excluded; run 35976721262 precedent). Watchdog was capped ~10:02Z 2026-09-25 — restart if expired. -- convergeDevMark Read#2: the next real dev-session read; unread != zero. -- faultinject first scheduled run: 2026-10-01 03:00Z (verify PHASES_ARG/HEADLESS_ARG in dry-run echo log). -- Hard dates 2026-10-20: DbPool arm(iv) verdict, convergeDevMark dead-fallback, W8 fallback. -- Upstream probe issue status check if the user has posted it (R12-H1 artifact). - -## Binding negatives (D-001..D-005) - -- No capability/product-grade fork work until the Fork Line pinned trigger fires (per-request model-field-driven egress switching within a single account / node-level pinning). -- No external actions under the user's identity (issue/PR/fork creation) — user executes; agent only prepares. -- ADR-0067 D3 (loopback-REST mere aggregation) and ADR-0079 (engine-is-a-seam) unchanged; no new L7 in Mode A; protocol-family freeze holds. -- Observation data is not delivery evidence; delivery evidence remains CI-only (standing ruling, register R12-H section). -- `AGENTS.md` ~78B headroom to the 32KiB cap — do not add text. diff --git a/docs/agents/round12-wave-i-next-round.md b/docs/agents/round12-wave-i-next-round.md deleted file mode 100644 index 993aea03..00000000 --- a/docs/agents/round12-wave-i-next-round.md +++ /dev/null @@ -1,66 +0,0 @@ -# Round 12 Wave-I — Next-Round Taskbook - -Slug: `r12-wave-i-grill` | Baseline: `origin/main` 33dc0333 (wave-h landed) -Ledger: `.scratch/r12-wave-i-grill/decision-ledger.md` — D-001..D-007, all current. -Reports: `.codex-tmp/atomcode-wavei-q1..q7-{prompt,report}.md` (gitignored, session-local). - -## How to use this taskbook - -- Each task declares the D-xxx IDs it covers. The ledger is the source of record. -- Register write-backs landed in the wave-i closeout commit; tickets below are the next duties round. -- Suggested skills: `$atomcode-research` (new adjudications only, serialized), `$gitbutler` (all VC writes), `code-review` for the audit loop, `grill-with-docs / domain-modeling / neat-freak / handoff` for the next cycle. - -## R12-I1 — XFP trust-boundary tighten (D-002) - -- `src-tauri/src/headless_security.rs`: `trusted_forwarded_https` — merge all XFP header instances, trim, take LAST (rightmost) segment; value not in {http,https} => fail-closed. Peer-CIDR gate (R12-D1) untouched. -- `src-tauri/src/headless_main.rs`: `parse_trusted_proxy` rejects `0.0.0.0/0` + `::/0` unless a second explicit flag (suggested `--trusted-proxy-unrestricted`) is present; startup info line prints a visible warning when unrestricted. -- Tests: trusted peer + injected `https,http` must NOT yield Secure; `http,https` positive does; double-header-instance cases; flip the codified first-segment test. -- Docs: fix the "fail-closed" doc-comment to describe rightmost/nearest-hop semantics; HEADLESS_DEPLOYMENT.md 215-219 sync. -- Severity: hygiene-required, not a boundary defect (self-inflicted availability quirk). Zero new deps. N>1 trusted-hop depth YAGNI until needed. - -## R12-I2 — db-lock-metrics feature + dual threshold (D-003.1) - -- Promote the `#[cfg(debug_assertions)]` lock-wait instrument to opt-in cargo feature `db-lock-metrics` (default off = zero-cost retained). -- Dual thresholds: keep >=1ms existence tier, add >=5ms magnitude tier (fixes the fairness-line low-discrimination defect). -- Feature-on builds are for observation cycles only — NOT shipping config; CI adds a `--features db-lock-metrics` verify pass to guard feature-unification drift. -- Holder-identity attribution must land — the D-005 reopen-when condition depends on it. - -## R12-I3 — evidence convention enforcement (D-003.2 + D-007.1) - -- Closeout already created `docs/agents/evidence/` + README + wave-h backfill. -- This ticket: machine gate — extend or clone the `waiver-ledger-check.mjs` pattern so new trigger-line instrument entries lacking the three declarations (observation environment / zero-read power / evidence class) fail verification. - -## R12-I4 — test-quality trio (D-004) - -- Move p99<=50ms and the 250ms probe bound out of the push gate into bench.yml artifacts (benchmark bodies still run = correctness regression still caught; duration assertions leave the gate — djust #2157 precedent). On landing, decrement the p95-flake line's covered-assertion count in the register. -- Demote the `contains("TransactionBehavior::Immediate")` source-read to a scripts/ source-gate with comment-stripping + whitespace normalization (keyhog-scanner pattern); add the two-connection behavioral test (A holds write txn -> B replace_ports asserts BUSY/non-upgrade path; strict ordering, no racing). -- Un-gate `file_backed_reads_use_the_dedicated_read_conn`: make it observable via public API or a SEPARATE test feature (not db-lock-metrics — keep the observation-only boundary). Instrument self-check tests stay debug-gated. - -## R12-I5 — convergeDevMark comment re-scope (D-003.3) - -- Update the instrument comment in `src/store/appStore.ts` (~:241) to declare overlap-only semantics: zero readings prove zero-overlap, NOT churn-free; sequential-duplicate detection requires the parent line to define redundancy semantics first. (Register row already updated in closeout.) - -## R12-I6 — strategy_service split, dedicated wave (D-006.1) - -- Dedicated duties wave, hard anchor ~2026-10-10. Three steps: (1) shared types first (ReconcileMemory etc. = the seam — ReconcileMemory is NOT pure read-plane; first step is `find_references ReconcileMemory` to confirm seam edges); (2) read plane (plan/get/ReconcilePlan); (3) write plane (apply/reconcile — may merge into the 10-20 batch). -- Scope fence: moves + re-exports only, zero behavior change, same tests before/after; stop rule = read plane + shared types landed. Do NOT touch the 4000-line budget to fit. - -## R12-I7 — i18n codegen eval (D-006.2) — 2026-10-20 block - -- Evaluate checked-in generated file + verify diff gate (gen script from `src/locales/*/common.json` -> `tray_labels.gen.rs` committed; verify-build regenerates + `git diff --exit-code`). build.rs rejected (OUT_DIR unlintable). First check whether `tauri-plugin-i18n` already solves the shared-source problem. - -## R12-I8 — observation roll-call - -- s1 second scheduled green (~2026-09-26 09:00Z): close with corroboration note citing both runs (D-007.2). -- convergeDevMark Read#2 on next real GUI session. -- faultinject first scheduled run 2026-10-01 03:00Z. -- 2026-10-20 hard dates: DbPool arm(iv), convergeDevMark dead-fallback (with postmortem note if dead), W8 fallback, i18n-codegen eval. -- Upstream probe issue status (user-side; dormant-fork clock starts at submission). - -## Binding negatives (D-001..D-007) - -- No external actions under user identity (issue/PR/fork) — user executes. -- No behavior changes inside the split wave; no build.rs for i18n; no new deps for XFP; feature-on builds never ship as delivery config. -- Observation data is not delivery evidence; delivery evidence CI-only. -- No new trigger line for reader-vs-reader (covered-by note on existing line only). -- `AGENTS.md` ~78B headroom — no additions. diff --git a/docs/agents/round12-wave-j-next-round.md b/docs/agents/round12-wave-j-next-round.md deleted file mode 100644 index d48586cc..00000000 --- a/docs/agents/round12-wave-j-next-round.md +++ /dev/null @@ -1,57 +0,0 @@ -# Round 12 Wave-J — Next-Round Taskbook - -Slug: `r12-wave-j-grill` | Baseline: `origin/main` 12062c87 (wave-i landed) -Ledger: `.scratch/r12-wave-j-grill/decision-ledger.md` — D-001..D-008, all current. -Reports: `.codex-tmp/atomcode-wavej-q{1..7}-{prompt,report}.md` (gitignored, session-local). - -## How to use this taskbook - -- Each task declares the D-xxx IDs it covers. The ledger is the source of record. -- Register write-backs landed in the wave-j closeout commit (4 armed rows + R12-J section + Procedure bullet); tickets below are the next duties round. -- Suggested skills: `$atomcode-research` (new adjudications only, serialized), `$gitbutler` (all VC writes), `code-review` for the audit loop, `grill-with-docs / domain-modeling / neat-freak / handoff` for the next cycle. - -## R12-J1 — audit.rs single critical section (D-002) - -- `crates/resin-core/src/audit.rs`: `AuditLog::append` — hold the `last_hash` lock across prev_hash read -> serialize -> rotate -> write_row -> commit (one critical section; the type itself becomes thread-safe, not the callers). -- Concurrency regression test (CI domain): shared private AuditLog instance, N threads append concurrently, chain continuity verified (no two rows share prev_hash). -- Comment rewrite at the last_hash site: per-instance thread-safety now enforced by the lock; the same-file single-instance invariant stays documented (cross-instance concurrent writers to one file have no intra-process solution). -- argus rule unchanged: append failure still degrades to tracing::error + Ok(()). -- Note for the implementer: I/O inside the lock adds zero contention — the global AUDIT mutex already serializes the whole append including fsync. - -## R12-J2 — macOS test leg: tier-2 signal + release gate (D-004) - -- Non-gating signal: add `macos-latest` to bench.yml `matrix.os` (cargo test already runs in that leg; monthly cron + dispatch = periodic cost, not per-merge). -- Release gate: macOS cargo test as a required check inside the workflow_dispatch release path in ci.yml — gate-at-release, signal-at-main; never on push. -- Register armed row (escalation on first macOS-unique failure) landed in closeout. -- Do NOT add macOS to the push-gate verify job — ~10x runner cost + flake surface on the only merge gate (ADR-0072 noise discipline). - -## R12-J3 — snapshot.rs prod-lines gate (D-005) - -- `scripts/verify-build.sh`: new gate after the strategy_service guard — count prod lines of `crates/resin-core/src/snapshot.rs` excluding the `mod tests` block (anchor: the `mod tests` marker at ~:919; count lines before it); FAIL at >2000 (2.2x headroom ratchet, ~919 today). -- The armed register row landed in closeout; this ticket lands the enforcement. Keep the measured figure consistent with the register row wording. - -## R12-J4 — hygiene batch: TopologyView `?s:` (D-007) - -- `src/views/TopologyView.tsx:97-101` — replace `? {} :` with `? s :` in the five shallow-guard setters. zustand bails on Object.is(nextState, state) checked on the returned partial before merge; `{}` always produces a new root and notifies (v5.0.14 source-verified). -- The parked (⑤c) file-churn register row fires on this edit and discharges with it. -- Hygiene-batch admission only: do not touch the useShallow subscriber convention; no data-flow refactor; no standalone ticket. - -## R12-J5 — observation roll-call (bookkeeping) - -- s1 second scheduled green (~2026-09-26 09:00Z): on landing, close the holding note citing both scheduled runs (r12-wave-i D-007.2; mechanism-fix exception clause not needed - this wave was pre-scheduled). -- convergeDevMark Read#2 on the next real GUI session (overlap-only scope). -- faultinject first scheduled run 2026-10-01 03:00Z. -- ~2026-10-10: strategy_service split wave (R12-I6, unchanged). -- 2026-10-20 hard dates: DbPool arm(iv) verdict, convergeDevMark dead-fallback (postmortem if dead), W8 fallback, i18n-codegen eval. -- User-side: upstream probe issue (dormant-fork clock starts at submission); packaging acceptance (tag + push + workflow_dispatch). - -## Binding negatives (D-001..D-008) - -- No file locks / cross-process audit protection (out of scope); argus degrade semantics frozen. -- No port-probe rework: no held-socket transfer (architecturally impossible across the whitebox boundary), no self-healing port swap (config contract); `suggest_` naming stays. -- No macOS in push-gate verify; no unconditional platform-test exemption (exemption without triggers = silent drift). -- No snapshot split (dilutes the ADR-0051 single-merge-point audit surface); snapshot is NOT eligible for the ~10-10 wave (scope fence: read-model != write-orchestration debt). -- No ipc.ts domain split without usage evidence; specta runtime-gen stays rejected (ADR-0053; sole reopen = middleware hooks carrying trace_id+dual-mode). -- No LOC gate on ipc.ts (interface-role drift only); no TopologyView data-flow refactor. -- Composition-check pre-filter gates agenda entry only — never substitutes adjudication. -- Armed/parked lines without new machine evidence are not re-grilled. diff --git a/docs/agents/round12-wave-k-next-round.md b/docs/agents/round12-wave-k-next-round.md deleted file mode 100644 index b219d1d8..00000000 --- a/docs/agents/round12-wave-k-next-round.md +++ /dev/null @@ -1,52 +0,0 @@ -# Round 12 Wave-K — Next-Round Taskbook - -Slug: `r12-wave-k-grill` | Baseline: `origin/main` 7be89b87 (wave-j duties landed + audited; merge-red resolved by rerun) -Ledger: `.scratch/r12-wave-k-grill/decision-ledger.md` — D-001..D-002, all current. -Reports: `.codex-tmp/atomcode-wavek-q{1,2}-{prompt,report}.md` (gitignored, session-local). -Evidence: `docs/agents/evidence/r12-wave-k/` (observation class, not delivery evidence). - -## How to use this taskbook - -- This wave produced NO implementation tickets — it is watch-only. Each item declares the D-xxx IDs it covers; the ledger is the source of record. -- Register write-backs landed in the wave-k closeout commit (2 armed rows + R12-K section). -- Suggested skills: `$atomcode-research` (new adjudications only, serialized), `$gitbutler` (all VC writes), `code-review` dual-axis before commit, `grill-with-docs / domain-modeling / neat-freak / handoff` next cycle. - -## R12-K1 — wait_bound flake watch (D-001, D-002.2) - -- Armed register row: same-signature main verify failure (`bound set did not reach want=`) -> rerun once -> green=count=2 -> fix ticket (event-signal or budget extension via wave-i D-007 mechanism-fix channel); red=count=2 immediately. -- TTL bound to the 10-20 block: still poll-structured with no recurrence -> auto-promotes to hygiene ticket (wait_bound event-ification). -- Do NOT proactively fix now (count=1, >=2 threshold precedent; no-prepay). - -## R12-K2 — faultinject first run (carry, ~2026-10-01 03:00Z) - -- First scheduled monthly run produces machine evidence; check run + register row on/after the date (heartbeat line: absent >=2 consecutive occurrences -> fires). - -## R12-K3 — macOS tier-2 signal watch (carry, wave-j D-004) - -- First macOS-unique test failure on the non-gating bench leg -> promote armed row to adjudication. One green on record (36255905315). - -## R12-K4 — strategy_service split wave prep (~10-10, R12-I6) - -- Unchanged. Wave-j D-008 composition-check applies at agenda entry: prod/test split + duties-per-line before any LOC charge earns a slot. Snapshot.rs is scope-fenced OUT (read-model != write-orchestration). - -## R12-K5 — 10-20 hard block (carry + TTL join) - -- DbPool arm(iv) verdict / convergeDevMark dead-fallback (postmortem if dead) / W8 fallback / i18n-codegen eval / **NEW: wait_bound TTL check** (R12-K1). - -## R12-K6 — roll-call / user-side (D-001.5) - -- convergeDevMark Read#2 on next real GUI session. -- User-side: upstream probe issue submission (dormant-fork clock at submission); packaging acceptance (tag + push + workflow_dispatch — macos-test required-check now guards that path). -- snapshot prod-lines ratchet watch: 918/2000. - -## Armed lines fired by next source merge - -- **delta-scan** (D-001.4/D-002.3): first post-7be89b87 merge touching src/, src-tauri/, crates/, scripts/, or src/locales/ -> scan frozen range 33dc0333..7be89b87 only. - -## Binding negatives (D-001..D-002) - -- No critique v3 full rescan without the reopen precondition (false-positive stats or detector swap). -- No in-test retry / auto-quarantine additions (ADR-0072 noise discipline; legislated absence). -- No proactive wait_bound fix before count=2 or TTL expiry. -- Armed/parked lines without new machine evidence are not re-grilled. -- All register armed rows carry obs-env:/zero-read:/evidence: — the instrument-declaration gate enforces. diff --git a/docs/agents/round9-next-round.md b/docs/agents/round9-next-round.md deleted file mode 100644 index 5fbf8e04..00000000 --- a/docs/agents/round9-next-round.md +++ /dev/null @@ -1,137 +0,0 @@ -<!-- - Source-of-truth path: .scratch/round9-grill/handoffs/next-round.md (session artifact) - This docs/agents/ copy is the persistent, git-tracked reference. - Generated: Round 9 grill closeout (D-001..D-010 settled; 11 sharp-critique friction points adjudicated). - Regenerate this file from the session artifact after any ledger update. ---> - -# Round 9 Next-Round Task Book - -> **数据源纪律**:本任务书唯一数据源是 `.scratch/round9-grill/decision-ledger.md`(D-001..D-010 current),不从对话回忆补充任何结论。 -> **grill 终裁**:锐评全部 11 项摩擦点(P0×4 + P1×6 + P2×1)已决策。 - ---- - -## 任务清单(按立票优先级排序) - -### Ticket R9-01: ADR-0068 D4 四场景 + t17 合同验证挂入 CI(小票) - -- **覆盖决策**: D-001 -- **范围**: ADR-0068 D4 四场景(HTTP 503 NO_AVAILABLE_NODES / SOCKS5 05-00 成功 / SOCKS5 05-ff 拒绝 / HTTP 403 ENDPOINT_CAPABILITY_DISABLED)+ ticket 17 合同验证(repro/t17-contract)挂入 verify-build.sh 或 CI release checklist -- **工作量**: 半天 -- **风险**: 低(挂入既有门禁) -- **依赖**: 无 - -### Ticket R9-02: EffectiveConfigView 补显 Live policy(小票) - -- **覆盖决策**: D-002 -- **范围**: `src/views/EffectiveConfigView.tsx` Consistent 行渲染 `resin_allocation_policy` + 18 locale 各 1-2 key + vitest 断言 -- **工作量**: 半天 -- **风险**: 低(纯渲染 + i18n) -- **依赖**: 无 - -### Ticket R9-03: Headless guard 接线恢复 + 回归锁 + CI 补洞(P0) - -- **覆盖决策**: D-003 -- **范围**: - 1. 从 `git show f18f365b:src-tauri/src/headless_main.rs` 取回 146 行(import + CLI auth_token/allowed_host + resolve_token 调用 + HeadlessGuard::new + .layer + security_guard 中间件) - 2. 新集成测试断言 build_router 对无 token /api/v1/platforms 返回 401/403、对坏 Host 返回 403 - 3. verify-build.sh push 路径加 `cargo check -p egressapikey-app --features headless` - 4. P0③ 残余 runtime 复测(GET query 透传 + body 双包体) -- **工作量**: 1-2 天 -- **风险**: 低(恢复已审阅落板代码;primitives + 14 单测幸存;`git log f682940e..HEAD` 为空可直接 cherry-pick) -- **依赖**: 无 - -### Ticket R9-04: reset_kernel 接现成 restart_resin(小票) - -- **覆盖决策**: D-004 -- **范围**: `src-tauri/src/commands/settings.rs` sidecar_restart 改为调用 `egressapikey_app::sidecar::restart_resin(app)` via spawn_blocking(启用当前弃用的 _app 参数) -- **工作量**: 1-2 小时(~20 行) -- **风险**: 极低(复用 sidecar.rs:532-586 已测路径 + round8-07 4 单测) -- **依赖**: 无 - -### Ticket R9-05: subscription_remove 补清理 + ensure_default_port 补偿(P1) - -- **覆盖决策**: D-007 -- **范围**: - - 1. subscription_remove 补清理逻辑(remove_default_port_if_orphaned,port_list 过滤 platform_id 匹配 → port_remove 正常路径) - 2. ensure_default_port 补偿检查(同 platform 已有 default port 跳过创建,幂等) - 3. 单测 + 集成测试断言孤儿不留存 - -- **工作量**: 1-2 天含测试 -- **风险**: 低(Mode A 已无孤儿面 ticket 17,修复仅针对 Mode B 残留) -- **依赖**: 无 - -### Ticket R9-06: 工程边角 5 项批量修复(P2) - -- **覆盖决策**: D-010 -- **范围**: - - 1. **#1+#2 必修**(1h): verify-build.sh L9-11 非 Windows CI 分支追加 2 行 cargo check headless+custom-protocol - 2. **#3 必修**(10min): 改 `02-headless-adapter-report.md:193` "43 条"→"44 个" - 3. **#7 必修**(30min): verify-build.sh 加 codegraph status 或 AGENTS.md 降格 "P23 硬闭环"→"建议 codegraph sync" - 4. **#5 可选**(半天): readme-lang-check.cjs 加第 18 check(EN/CN code fence 语言标识镜像) - 5. **#8 可选**(半天-1天): vitest-isolation-guard.cjs 扩全部 `src/views/*.test.tsx` + 泛化 pattern - -- **工作量**: 约 1 天(必修 2h + 可选 0.5-1 天) -- **风险**: 低(必修项改动小,可选项与 R9-03 #3 CI 补洞有部分重叠可合并) -- **依赖**: 无 - ---- - -## 范围外记录(无需立票) - -| 决策 | 范围外理由 | -|------|-----------| -| **D-005**(三层一致性)| 维持现状,ADR-0069 D1 已立案(L2-first + level-triggered reconcile)| -| **D-006**(幂等 vs 强制)| 推迟 + 观察哨,等实证案例再立票 | -| **D-008**(配置导入原子性)| 推迟,validate-both + 自动备份 + 双 rollback 兜底已足 | -| **D-009**(WebDAV 恢复)| 已完成,ADR-0070 闭环齐全 | - ---- - -## 总工作量预估 - -- **R9-01**(小票): 半天 -- **R9-02**(小票): 半天 -- **R9-03**(P0): 1-2 天 -- **R9-04**(小票): 1-2 小时 -- **R9-05**(P1): 1-2 天 -- **R9-06**(P2): 约 1 天 -- **合计**: 约 4-5 天 - ---- - -## Suggested skills(按票优先级) - -| Ticket | Suggested Skills | -|--------|-----------------| -| R9-01 | to-spec / to-tickets / implement(含 tdd)/ code-review | -| R9-02 | to-tickets / implement(含 tdd)/ i18n-check guard | -| R9-03 | diagnosing-bugs / implement(含 tdd)/ code-review / but cherry-pick | -| R9-04 | implement(含 tdd)/ code-review | -| R9-05 | to-spec / to-tickets / implement(含 tdd)/ code-review | -| R9-06 | to-spec / implement(含 tdd)/ code-review | - -通用 skill: - -- **but**:版本控制唯一面(commit / branch / cherry-pick / verify-build 闭环) -- **codegraph**:探索前先 codegraph_explore(避免重复 Read) -- **ctx_***:文件编辑/分析/抓取全程 node.js 写文件 -- **atomcode-research**:新调研先 ctx_search 既有存档再开 - ---- - -## 数据源引用 - -- 账本:`.scratch/round9-grill/decision-ledger.md`(D-001..D-010 current) -- 锐评:`.codex-tmp/rui.txt`(P0×4 + P1×6 + P2×1 = 11 项摩擦点) -- 上轮归档:`.scratch/architecture-recovery-closed-2026-09-16/`(Round 8 归档,含 A-001..021 决策) -- 上轮 handoff:`handoff-round8-closed-2026-09-16`(ctx 知识库) - ---- - -**生成时间**: Round 9 grill 整理阶段(用户消息:"继续") -**数据源**: decision-ledger.md(10 条 current,无 revised) -**对账闸**: 通过(无去向清单为空) diff --git a/docs/agents/storage-locations.md b/docs/agents/storage-locations.md index ad5bb392..052bb181 100644 --- a/docs/agents/storage-locations.md +++ b/docs/agents/storage-locations.md @@ -5,7 +5,7 @@ section keeps the load-bearing rules and a pointer here. - **L1 GUI preferences (`settings.json`)**: `app_config_dir()` — written by `tauri-plugin-store` (webview `src/lib/settings.ts`) plus six Rust commands (`lightweight_get/set`, `get/set_diag_poll_interval` (Round 5 T05), `get/set_log_level`, all in `src-tauri/src/commands/settings.rs`) for keys the webview cannot own. Keys: `lang`, `theme`, `view`, WebDAV credentials (`webdavUrl`/`webdavUsername`/`webdavPassword`), IP-reputation credentials (`ipReputationProvider`/`ipQualityScoreApiKey`/`abuseIpDbApiKey`; note: these third-party providers supply fraud scoring + abuse history — Resin's built-in GeoIP (`/api/v1/geoip/*`) is deliberately NOT consumed, see ADR-0065), `ipChannelPolicyMap`, `keyCandidates`, `nodeProbe`, `portAuthDefault`, `splitRatio`, `lightweightEnabled`/`lightweightDelayMinutes`, `diagPollInterval` (T05; set via `set_diag_poll_interval`, §7.5 range 100..=24h enforced on both the Rust and TS boundaries), `topologyState`/`localSubOrder`, `view` (last-active tab; `effectiveConfig` is the one-level Effective Config view, architecture-recovery ticket 13). Only the Rust tray reads `lang` directly; no other Rust component consumes L1 keys (see §7.6). Dead pre-T3-A network keys (`gatewayBind`, `mihomoApi`) are purged once at startup (arch/02). The legacy `processRoutes` key was migrated into the L2 whitebox and purged once at startup (arch/17, ADR-0055). L1 keys never influence proxy behavior — if a key changes what the proxy does, it belongs in L2. - **L2 whitebox user-editable config (authoritative write entry)**: two files in `app_config_dir()`, each with exactly one writing module — - - `egressapikey-strategy.json` (`resin_core::StrategyConfig`, version 1): shell strategy identity (`a_class`, `b_class`, `manual_nodes`, `subscriptions`, `top_n` per platform; since round8 ticket 01 / D-002 `b_class` holds the Resin `allocation_policy` value verbatim — the six display-only shell options were withdrawn, legacy tokens are accepted on read and rewritten once at boot by `StrategyService::migrate_b_class_values_once` with the generation untouched, and `b_class_params` is gone from both the UI and the API surface). Written only by `resin_core::StrategyService` (`strategy_config_put` + the deep `strategy_platform_regions_set` are its thin IPC facades; ADR-0052); applied to Resin only by `strategy_apply` (which fulfills the reconcile preview's promises: creates missing-on-resin platforms via `ResinClient::create_platform_from_name` per ADR-0056 and PATCHes only the axes that actually drift — `region_filters` and the desired `allocation_policy` (round8 ticket 01 added the policy axis; only drifting axes are sent) — diff-then-skip per ADR-0057, so a converged platform takes zero writes; it never deletes whitebox entries — the former auto-clean of stale platform entries was removed by ADR-0056 as the "said establish, actually deleted" contradiction; on an ALL-GREEN pass it writes back `applied_generation = generation` + `last_apply_at` and clears `last_apply_error` inside the same store entry, keeping the old applied value + recording the failure reason on any non-green pass per ADR-0058). Every write bumps the `generation` write-authority counter inside the write entry (v1 files deserialize with generation 0 = NeverApplied, serde-default zero migration; the ports sibling carries a single-generation `generation` only — no applied_generation, D-27). GUI edits and external file edits converge here (ADR-0036, read side per ADR-0039 SS2). The authoritative snapshot derives the top-level `ConvergePhase` (NeverApplied/Unknown/ApplyFailed/PendingApply/Drifted/Converged) from the pair and EffectiveConfigView surfaces it as the header chip; adding a config key to this file means updating that derivation story too (ADR-0058). The global converge loop (architecture-recovery 07, D-C2.2) lives in `src/store/appStore.ts`: one `subscribeToConverge()` subscription (mounted once in `App.tsx`) owns the `authoritative_snapshot` cadence — 5s foreground polling, 30s backoff once Converged AND `lastApplyAt` >60s old, immediate refresh on the existing `sidecar-status` G4 event (payload untrusted/unused), visibility-API pause while hidden; the tray mirrors the phase edge-driven from the `authoritative_snapshot` tail (`tray.rs apply_converge_mirror`: Converged silent / Drifted+PendingApply amber / ApplyFailed red until the next green apply / Unknown+NeverApplied grey; tooltip shows `{Phase} · rev {N}/{M}` for non-silent phases where N = strategy_generation and M = strategy_applied_generation; a "Converge status" menu item emits the `tray://converge-status` Tauri event to the frontend, which navigates to EffectiveConfigView) — no new IPC command, no new config key. Subscription establish-phase STATUS rows (round7 T02, D-C1.2): the top-level `subscriptions` array (each row = `name` + `phase` + `stage` + `phase_error`; the per-platform `PlatformStrategy::subscriptions` NAME refs are a different field) records where the establish cascade is for each subscription — written ONLY by `StrategyService::record_subscription_phase` (status subresource: same store entry, backup ring, audit row, but NO generation bump — a status write is not a desired-state write, k8s status-subresource rule; bumping would flip the top-level ConvergePhase into a false PendingApply after every cascade). `authoritative_snapshot` projects these rows as `subscriptionPhases` (camelCase parent, snake_case rows) and SubscriptionsView renders the 6-phase chip per subscription row. Every write is versioned: the previous file is copied to the sibling `backup/` dir (10 kept) and `strategy_backup_list`/`strategy_rollback` re-enter this same write entry for rollback (ADR-0054 §B). + - `egressapikey-strategy.json` (`resin_core::StrategyConfig`, version 1): shell strategy identity (`a_class`, `b_class`, `manual_nodes`, `subscriptions`, `top_n` per platform; since round8 ticket 01 / D-002 `b_class` holds the Resin `allocation_policy` value verbatim — the six display-only shell options were withdrawn, legacy tokens are accepted on read and rewritten once at boot by `StrategyService::migrate_b_class_values_once` with the generation untouched, and `b_class_params` is gone from both the UI and the API surface). Written only by `resin_core::StrategyService` (`strategy_config_put` + the deep `strategy_platform_regions_set` are its thin IPC facades; ADR-0052); applied to Resin only by `strategy_apply` (which fulfills the reconcile preview's promises: creates missing-on-resin platforms via `ResinClient::create_platform_from_name` per ADR-0056 and PATCHes only the axes that actually drift — `region_filters` and the desired `allocation_policy` (round8 ticket 01 added the policy axis; only drifting axes are sent) — diff-then-skip per ADR-0057, so a converged platform takes zero writes; it never deletes whitebox entries — the former auto-clean of stale platform entries was removed by ADR-0056 as the "said establish, actually deleted" contradiction; on an ALL-GREEN pass it writes back `applied_generation = generation` + `last_apply_at` and clears `last_apply_error` inside the same store entry, keeping the old applied value + recording the failure reason on any non-green pass per ADR-0058). Every write bumps the `generation` write-authority counter inside the write entry (v1 files deserialize with generation 0 = NeverApplied, serde-default zero migration; the ports sibling carries a single-generation `generation` only — no applied_generation, D-27). GUI edits and external file edits converge here (ADR-0036, read side per former ADR-0039 SS2 (record retired 2026-10-15)). The authoritative snapshot derives the top-level `ConvergePhase` (NeverApplied/Unknown/ApplyFailed/PendingApply/Drifted/Converged) from the pair and EffectiveConfigView surfaces it as the header chip; adding a config key to this file means updating that derivation story too (ADR-0058). The global converge loop (architecture-recovery 07, D-C2.2) lives in `src/store/appStore.ts`: one `subscribeToConverge()` subscription (mounted once in `App.tsx`) owns the `authoritative_snapshot` cadence — 5s foreground polling, 30s backoff once Converged AND `lastApplyAt` >60s old, immediate refresh on the existing `sidecar-status` G4 event (payload untrusted/unused), visibility-API pause while hidden; the tray mirrors the phase edge-driven from the `authoritative_snapshot` tail (`tray.rs apply_converge_mirror`: Converged silent / Drifted+PendingApply amber / ApplyFailed red until the next green apply / Unknown+NeverApplied grey; tooltip shows `{Phase} · rev {N}/{M}` for non-silent phases where N = strategy_generation and M = strategy_applied_generation; a "Converge status" menu item emits the `tray://converge-status` Tauri event to the frontend, which navigates to EffectiveConfigView) — no new IPC command, no new config key. Subscription establish-phase STATUS rows (round7 T02, D-C1.2): the top-level `subscriptions` array (each row = `name` + `phase` + `stage` + `phase_error`; the per-platform `PlatformStrategy::subscriptions` NAME refs are a different field) records where the establish cascade is for each subscription — written ONLY by `StrategyService::record_subscription_phase` (status subresource: same store entry, backup ring, audit row, but NO generation bump — a status write is not a desired-state write, k8s status-subresource rule; bumping would flip the top-level ConvergePhase into a false PendingApply after every cascade). `authoritative_snapshot` projects these rows as `subscriptionPhases` (camelCase parent, snake_case rows) and SubscriptionsView renders the 6-phase chip per subscription row. Every write is versioned: the previous file is copied to the sibling `backup/` dir (10 kept) and `strategy_backup_list`/`strategy_rollback` re-enter this same write entry for rollback (ADR-0054 §B). - `egressapikey-ports.json` (`resin_core::WhiteboxConfig`, version 2: `entry_ports` + `network` + `process_routes` + `route_acknowledged`; each `entry_ports` row declares exactly one of `http` / `socks5` / `mixed`, with `mixed` the default - round8 ticket 13 / D-007 closed the enum at three values and tightened the engine flag mapping, so `socks5` is SOCKS5-only and `mixed` is the only value that opens both capabilities; the closed set and the single flag derivation live in `crates/resin-core/src/entry_protocol.rs`, and a version-1 document is upgraded once at boot by `whitebox_config::migrate_entry_port_protocols`, which rewrites the flag-preserving `socks5` -> `mixed` and stamps version 2): written only by `crates/resin-core/src/whitebox_config.rs` `WhiteboxConfigStore` (hotswap-config atomic write + validate-before-swap + file watch). `port_upsert`/`port_remove`/`port_toggle`/`whitebox_save_network` and — since arch/17 (ADR-0055) — the process-route family `process_route_add`/`process_route_remove`/`process_route_list` (the ONLY writers for routes; the former L1 settings.json dual-write and the webview `saveProcessRoutes`/`loadProcessRoutes` pair are deleted, one-time boot migration purges the legacy L1 key) all funnel into it; its `watch_apply` applies accepted files to SQLite + listeners as one transaction (invalid files never trigger the callback). `egressapikey.db` (`DbPool`, `port_mappings` table, hand-written `PRAGMA user_version`, currently v4) is the SQLite sync partner seeded from the whitebox JSON at boot (`main.rs` seeds `WhiteboxConfigStore` from `db.list_ports()`; on corrupt JSON it quarantines and reseeds from DB) — the whitebox file, not the DB, is the truth source (ADR-0042 S2/S6). Writes are versioned the same way (sibling `backup/`, 10 kept; `whitebox_backup_list`/`whitebox_rollback` re-enter `WhiteboxConfigStore::apply` + `restore_ports_from_whitebox`, ADR-0054 §B). The reconciliation loop is closed (ADR-0054 ACCEPTED): one-shot drift tray notification is wired to the `authoritative_snapshot` command tail (`src-tauri/src/tray.rs` state machine; notify once per process, re-arm on zero unacknowledged drift, acknowledged entries exempt); user-facing troubleshooting table at `docs/how-to/WHY-NOT-EFFECTIVE.md`. - **L3 Resin runtime (derived, rebuildable)**: the sidecar's own state under the per-user Resin state dir (`state.db`/`cache.db`/`request_logs*.db`) plus live leases/listeners. Execute-only authority: reachable exclusively through the ResinClient REST seam (`crates/resin-core/src/resin_client.rs`); rebuildable from L2 at any time (`strategy_apply` PATCHes `region_filters`, `restore_ports_from_whitebox` re-POSTs `/api/v1/endpoints` after a Resin restart in Mode B - in Mode A it retires stale per-port endpoints and reloads the shell listener set instead, ADR-0042 S6). Former seam exception CLOSED by architecture-recovery ticket 11 (2026-08-30): `request_log_tail` now reads via ResinClient `GET /api/v1/request-logs`; Resin v1.2.0 does expose this endpoint. No shell code may read Resin's private `request_logs*.db` files — a direct read reintroduced anywhere is a review blocker. Declared exception: `backup_create` reads `state.db`/`cache.db` read-only for zip packaging only (never writes; Resin's own mutex guards backup-time consistency) — listed alongside the `request_logs*.db` prohibition as the complete read-exception ledger (ARCHITECTURE.md config-authority known exception; ADR-0050-bis). diff --git a/docs/agents/trigger-line-register.md b/docs/agents/trigger-line-register.md index 726c0b1a..9b7977e7 100644 --- a/docs/agents/trigger-line-register.md +++ b/docs/agents/trigger-line-register.md @@ -29,10 +29,10 @@ Re-open the trim adjudication when ANY line fires: | React Query migration (R12-01) | (a) concrete correctness/perf win found — measured duplicate-fetch or retry/ordering need; (b) a feature must touch polling + dispatch + write seams anyway | measuring (r12-wave-d D-005; cycle semantics legislated r12-wave-g D-001): R12-D4 lands a dev-only in-flight-overlap counter on both `ipcAuthoritativeSnapshot` trigger paths; >=1 measured overlap fires arm (a); an observation cycle = a REAL dev-session read of the counter - unread cycles are no-data, NOT zero (NOT_OBSERVED != 0); hard date 2026-10-20 - still no real read -> recorded dead/unevaluable (i18n line-2 dead-arm precedent) (docs/research/react-query-dual-channel-eval.md) | | bench-selfhosted `†` provisional thresholds | representative-hardware self-hosted run lands -> values become real gates | parked pending dispatch | | webview-smoke workflow | first post-merge run proves the stack; promote failure handling after evidence | **resolved 2026-09-23**: merged-state first run 35755345313 both legs green after R12-B3 selector fix (s1..s5 all PASS) - trigger fired and positively discharged | -| AGENTS.md 32 KiB ceiling | 32690/32768 B (78 B headroom, 2026-09-24 remeasure; supersedes stale 32756 figure): ANY net-addition trips `scripts/verify-build.sh`'s hard FAIL — the next substantive edit MUST externalize detail into `docs/agents/` (the §"Architecture & Phase History (externalized)" pattern) and leave a pointer. Do not raise the guard: 32 KiB is a consumer-side `project_doc_max_bytes` truncation limit, not an editorial choice | armed — trips on the next net-addition | +| AGENTS.md 32 KiB ceiling | 32690/32768 B (78 B headroom, 2026-09-24 remeasure; supersedes stale 32756 figure): ANY net-addition trips `scripts/verify-build.sh`'s hard FAIL — the next substantive edit MUST externalize detail into `docs/agents/` (the §"Architecture & Phase History (externalized)" pattern) and leave a pointer. Do not raise the guard: 32 KiB is a consumer-side `project_doc_max_bytes` truncation limit, not an editorial choice | RETIRED 2026-10-15 — governance-court ruling removed the build gate (AGENTS.md slimmed to ~1.7 KB); the 32 KiB consumer truncation limit remains real — convention-level, size check manual | | pending.diff/reason English strings in UI (F-10) | orchestration pending payloads render Rust English text raw — i18n-ify when a second consumer or a locale-quality complaint lands | recorded observation | | NODE_FC counter-window semantics (C8, r12-wave-b D-003) | Degraded platform finds ZERO candidates because every node flagged err >=1 occurrence -> rework to timestamped fixed-duration sliding window; verdict: tolerable-with-hole (conservative over-exclusion, no wrong switches). NOTE: no node-level self-heal path exists (cooldown is platform-level; unlike Envoy max_ejection_percent there is no evacuation floor) - the trigger IS the safety net | registered-exemption, armed | -| strategy_service split preemption (r12-wave-b D-001, extends wave-a D-003-7) | line-count headroom < 200 (currently 3636/4000 = 364 headroom) OR a feature ticket must touch the file -> split ticket preempts the feature ticket | parked, armed — critique `.codex-tmp/锐.md` logged as independent external evidence (r12-wave-d D-001); thresholds unchanged | +| strategy_service split preemption (r12-wave-b D-001, extends wave-a D-003-7) | line-count headroom < 200 (currently 3636/4000 = 364 headroom) OR a feature ticket must touch the file -> split ticket preempts the feature ticket | parked — armed-probe retired 2026-10-15 (governance-court removed the 4000-line build gate; headroom check now manual at grill, split obligation preserved in backlog). Critique `.codex-tmp/锐.md` logged as independent external evidence (r12-wave-d D-001); thresholds unchanged | | paired_request_added_latency_p95 flake (C9) | empirical count: >=2 observed flakes in CI -> promote to fix ticket (isolated passes; timing-sensitive under parallel load) | observing (0 confirmed hits this round as of 2026-09-22 - the only observed CI failure was webview-smoke, a distinct harness defect fixed under R12-B3). Covered-assertion decrement (r12-wave-i D-004): the 2 push-gate duration asserts that policed timing-flake surface (replace_ports p99 <= 50ms; lock-wait p99 < 250ms) moved to bench.yml dbprobe measure-only artifact rows - push-gate duration coverage 2 -> 0 | | forwarder port-scan O(n) suggestion path (critique #2) | measured suggestion-latency regression OR >=1 allocation-conflict event in a real deployment -> ticket | armed (r12-wave-d D-005) | | DbPool single-lock (bb8) (critique #6) | arm (ii) FIRED 2026-09-24 (probe p99=9615us/200 samples, run 35959139983) -> adjudicated r12-wave-f D-002: **keep-Mutex verdict** (synthetic saturation contention != production impact; Mutex-writer topology externally confirmed: emschwartz 95ms vs split-pool 83ms; abseil microbench discipline). Remaining arms: (i) dev lock-wait counter = PRIMARY instrument - real-load wait >=1ms -> reopen pool-impl (successor deadpool-sqlite, arm(i)-gated, B-read-conn first then A; r2d2 404-dead); (iii) dev counter zero for two observation cycles -> closed verdict; (iv) hard date 2026-10-20: verdict MUST land - no arm(i) corroboration -> closed-verdict archive | armed - arms i/iii/iv live, arm (ii) consumed; probe demoted to regression/A-B instrument (r12-wave-f D-002; supersedes r12-wave-e D-004/r12-wave-d D-005 wording). ARM (i) DATA 2026-09-25: instrument fired in a real dev GUI session - db WARN "DbPool lock wait >= 1ms" x2 (wait_micros=3731 @02:55:20Z, =12551 @03:00:05Z; cargo-run debug + live resin, no synthetic probe) - first real-load corroboration on record; ADJUDICATED r12-wave-h D-004: arm(i) FIRED - existence-based (parking_lot fair-unlock forces a >1ms holder at 12551us wait); magnitude caveat on record (dev-profile distortion partial, sqlite C lib prebuilt O2; rust#135082 dissent logged) -> pool-impl EVALUATION reopened, not migration (R12-H2); arm (iii) zero-path blocked; arm (iv) 2026-10-20 unchanged | @@ -43,7 +43,7 @@ Re-open the trim adjudication when ANY line fires: | macos-x86_64 delivery demotion (r12-wave-e D-003) | hard date 2027-08-01 (macos-15-intel retirement) -> A2 input-gate auto-escalates to A1 full-removal adjudication; demand line >=3 non-bot issues requesting Intel dmg -> re-entry adjudication; universal2 dmg (arm64 runner + x86_64 target + lipo) recorded as designated successor when the hard date fires (not implemented) | armed (implementation: R12-E2 input-gate + macos-15-intel migration) | | DbPool read-path unlock (P3, r12-wave-f D-002) | measured headless/VPS read latency attributable to the shared lock (NOT synthetic-probe data) -> land option B: a long-lived LazyLock read-only Connection serving list_ports/get_port (hynek -shm warning: MUST be long-lived - short-lived readers hit SQLITE_BUSY on open/close; snapshot_db_readonly precedent is file-level snapshot, semantics not transferable) | parked -> EVALUATION REOPENED r12-wave-h D-004 (arm(i) fired): R12-H2 step1 = long-lived LazyLock read-only Connection proto + real-session A/B; still gated sequencing, no migration commitment. READER-VS-READER note (r12-wave-i D-005): single read conn serializes concurrent readers - covered-by: step2 deadpool (wave-h D-004 successor); reopen-when: real-session read-conn wait >=1ms with holder=read-guard attribution (depends on wave-i D-003 holder-attr landing; dev-profile distortion caveat applies to read-wait reads too - small 1-3ms reads are a fresh adjudication call) | | census extractor AST upgrade (r12-wave-f D-003) | ANY of: (a) first proc-macro dependency lands in resin-core; (b) census expands to 3+ files; (c) >=2 regex qualifier-shape fixes within a year -> re-adjudicate AST extraction (syn/rustdoc JSON). Rejected now: nightly toolchain coupling + format drift (cargo-public-api #858, rust #135600) violates ADR-0072 minimal CI surface; known residual blind spots (same-line #[attr] fn; glued extern { fn }) enumerated in census.json residual_limits (r12-wave-g D-001) | armed | -| snapshot.rs prod-lines gate (r12-wave-j D-005) | prod-lines excluding the `mod tests` block > 2000 -> verify-build.sh FAIL -> split re-adjudication (2.2x headroom ratchet; the "second responsibility" reopen-when is narrative-only, not machine-checkable) | armed - gate live in scripts/verify-build.sh since R12-J3 (prod-lines measured 918 @2026-09-26, FAIL at >2000) | +| snapshot.rs prod-lines gate (r12-wave-j D-005) | prod-lines excluding the `mod tests` block > 2000 -> verify-build.sh FAIL -> split re-adjudication (2.2x headroom ratchet; the "second responsibility" reopen-when is narrative-only, not machine-checkable) | RETIRED 2026-10-15 — governance-court removed the verify-build.sh gate; line-count check now manual, split re-adjudication obligation moves to backlog (prod-lines 918 @2026-09-26) | | ipc.ts interface-role drift (r12-wave-j D-006) | app code imports ipc.ts directly bypassing domain modules, OR CMD_TO_HTTP coverage drifts beyond the manifest gate enforcement -> re-adjudicate domain split (LOC gate deliberately NOT armed - line count is duty concretized, not drift) | armed | | macOS test-signal leg (r12-wave-j D-004) | first macOS-unique test failure on the non-gating leg -> promotion-to-gating adjudication (tier-2 -> tier-1 re-tier) | armed - legs live since R12-J2: bench.yml macos-latest periodic signal + ci.yml macos-test release gate | | suggest_free_entry_port squatter escalation (r12-wave-j D-003) | a real squatter-hold event observed (entry port stuck retrying under a permanent external occupier) -> explicit per-port health-status ticket | armed | @@ -100,7 +100,7 @@ Re-open the trim adjudication when ANY line fires: ## R12-I observations and verdicts (r12-wave-i write-back) -- **STANDING PROCEDURAL RULES (r12-wave-i D-007)**: (i) new trigger-line instruments MUST declare three fields at birth - observation environment (dev/release/feature domain), what a zero reading proves and does NOT prove, evidence-retention class; the declaration is enforced by a machine gate (R12-I7 ticket - waiver-ledger-check pattern), not convention alone. (ii) covered-by notes must carry a machine-checkable reopen-when condition or they degrade into delay excuses. (iii) mechanism-level fixes MAY discharge a flake line at a single scheduled green - explicit exception for future use; s1 itself still runs the full 2-green window. +- **STANDING PROCEDURAL RULES (r12-wave-i D-007)**: (i) new trigger-line instruments MUST declare three fields at birth - observation environment (dev/release/feature domain), what a zero reading proves and does NOT prove, evidence-retention class; the declaration is enforced by a machine gate (R12-I7 ticket - waiver-ledger-check pattern), not convention alone. (Machine enforcement retired 2026-10-15 by governance-court ruling — convention-level since.) (ii) covered-by notes must carry a machine-checkable reopen-when condition or they degrade into delay excuses. (iii) mechanism-level fixes MAY discharge a flake line at a single scheduled green - explicit exception for future use; s1 itself still runs the full 2-green window. - **s1 post-land verification**: OBSERVATION closed 2026-09-26 - green #1 run 36116165114 @2026-09-25 09:01:59Z + green #2 run 36230828151 @2026-09-26 08:47:15Z (both schedule-event, main, both legs green). Corroboration standard met; the poll instrument remains permanent. - **Resin upstream probe issue**: OBSERVATION (user-side pending) - package at docs/agents/wave-h-upstream-package.md; dormant-fork trigger clock starts at submission (wave-h D-002). - **convergeDevMark Read#2**: OBSERVATION (renewed) - one more real dev-session read needed; re-scoped overlap-only (see R12-F row). @@ -159,7 +159,7 @@ Re-open the trim adjudication when ANY line fires: - Per-round grill: re-check each line's probe column; mark hits. - A hit line re-enters the normal ticket filter (D-003 four-tier rule). - Adding a suspended item without a named trigger line is a ledger error. -- New instruments and new armed trigger rows MUST declare at registration: observation environment, zero-read power boundary, evidence-retention class - literal markers `obs-env:` / `zero-read:` / `evidence:` in the entry text (r12-wave-i D-007; enforced by scripts/instrument-declaration-check.cjs in verify-build.sh). +- New instruments and new armed trigger rows MUST declare at registration: observation environment, zero-read power boundary, evidence-retention class - literal markers `obs-env:` / `zero-read:` / `evidence:` in the entry text (r12-wave-i D-007; machine enforcer retired 2026-10-15 by governance-court ruling — convention-level since). - A covered-by note without a reopen-when condition is a ledger error (r12-wave-i D-005). - Mechanism-level fixes may discharge a flake line at one scheduled green (r12-wave-i D-007 exception clause). - **Composition-check pre-filter (r12-wave-j D-008)**: a quantitative-headline charge (line counts, "N-times shell", O(n) claims) must pass a composition check BEFORE it earns an adjudication slot - production-vs-test split, duties-per-line, existing absorption/mitigation mechanisms. Charges failing the check dispose as closed-observations, not full adjudications. The filter gates agenda entry only; it never substitutes adjudication of the item itself. diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 153931d4..cbfdc27a 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -46,7 +46,7 @@ are deliberately unwired — selection rationale in ADR-0065. > This subsection legislates the three-layer configuration authority model. It > EXTENDS decided ADRs and reopens none of them: ADR-0036 (strategy whitebox -> single write entry), ADR-0039 SS2 (read-side strategy contract), ADR-0042 +> single write entry), ADR-0039 SS2 (read-side strategy contract; record retired 2026-10-15), ADR-0042 > S2/S6 (whitebox port truth + restore from whitebox). Where this subsection and > an ADR appear to conflict, the ADR wins and this subsection gets fixed. diff --git a/docs/history/README.md b/docs/history/README.md deleted file mode 100644 index f6413ce4..00000000 --- a/docs/history/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# docs/history/ — HISTORICAL ARCHIVE - -> **Not current truth.** These are session artifacts and phase plans from completed work. -> For current architecture, see `docs/architecture/`. For decisions, see `docs/adr/`. - -## Structure - -| Subdir | Contents | What it is | -|---|---|---| -| `phases/` | GRILL_T*, T4/T7, PROJECT_PLAN, BACKLOG | Done phase plans and execution backlogs | -| `handoffs/` | HANDOFF_* | Session handoff documents (agent-to-agent) | -| `prompts/` | USER_PROMPT_ARCHIVE, T9_MASTER_GOAL_PROMPT | Prompt archives, not engineering docs | - -## Why this exists - -Per the governance principle: constraints stay, history moves. -Phase plans and handoffs are point-in-time artifacts; they record *what was done*, not *what to do now*. -Archiving them here keeps the active docs surface clean for current and future contributors. diff --git a/docs/history/handoffs/HANDOFF_PATH_A.md b/docs/history/handoffs/HANDOFF_PATH_A.md deleted file mode 100644 index 99354e38..00000000 --- a/docs/history/handoffs/HANDOFF_PATH_A.md +++ /dev/null @@ -1,145 +0,0 @@ -# Handoff — Path A (ARCHIVED: pre-G1 spec) - -> **Status**: All G1-G5 milestones in this document have been IMPLEMENTED and verified at source level. -> This file is kept for historical reference (the original spec). The live handoff is [HANDOFF_PATH_A_CONTINUATION.md](./HANDOFF_PATH_A_CONTINUATION.md). -> G1: sidecar.rs boot_resin ✅ | G2: resin_client.rs + commands/mod.rs 19 commands ✅ | G3: spawn_health_poll clear_os_proxy ✅ | G4: 5 tabs + sidecar-status listener ✅ | G5: ci.yml 5 jobs + fetch_resin ✅ - ---- - -# Handoff — Path A 实施移交(2026-07-29) - -> 接手 agent 必读: -> 1. AGENTS.md(项目宪法,先读后做) -> 2. docs/MEMORY_REUSE_DECISION.md(含 A vs C 决策复审,决策=A) -> 3. docs/ARCHITECTURE.md、docs/PROJECT_PLAN.md -> 4. 本文件 -> 接手后第一动作:`codegraph sync .` 再做任何代码读。 - -## 上一窗口交付的成果 - -1. **决策复审固化到 docs/MEMORY_REUSE_DECISION.md**:A vs C 三维裁决,采用**路径 A**——fork Resin Go 二进制作为 Tauri sidecar 嵌入,Rust 壳仅做生命周期编排 + Ghost 安全网 + mihomo 控制。 -2. 6 个用户报告 bug 已修并推送(commits 4bb143a/a183287/fd0090d + 本窗口 fe6cc33)。 -3. AGENTS.md §11 已写入「runtime wiring state(post-#6 phase 1 sync)」,明确标注 **Resin 代理运行时尚未实现**——这是 phase 4 要接手的目标。 -4. 评估全过程产物持久在 `.omx/goals/autoresearch/go-vs-rust-path/{mission.json,rubric.md,ledger.jsonl,completion.json}`,可审计。 - -## 当前架构盘点(codegraph 已 sync,9 文件已变更) - -### Rust 侧 -- `crates/resin-core/src/lib.rs`:导出 `CoreConfig{lanes,bind,mihomo_api,mihomo_secret}`,`DEFAULT_LANES=10`、`MAX_LANES=50`、`sanitize_lanes()`。这是**重写残留**,phase 4 将大幅砍除。 -- `crates/resin-core/src/gateway.rs`:`GatewayState` 状态机(reserve/release/evict_lane/record_latency/snapshot)。**无 axum listener、无 SSE forward 实现**。phase 4 砍为「Forward client wrapper」。 -- `crates/resin-core/src/{lease,tdewma,platform,lane,mihomo}.rs`:1370 LoC 从零重写。phase 4 保留 mihomo.rs(mihomo 控制 wrapper),其余弃用或仅保留数据传输结构(DTO)。 -- `src-tauri/src/main.rs`:`.setup()` 读取 settings.json 注入 CoreConfig,trace + drop。**未启动 sidecar**。 -- `src-tauri/src/commands/mod.rs`:14 个 `#[tauri::command]`(见下方清单)。全部委托给 in-process kernel,未委托给 Resin。phase 4:大部分改为 forward → Resin REST。 -- `src-tauri/src/tray.rs`:build_tray + apply_labels(i18n)、`tray_refresh_labels` 命令。 - -### 前端侧 -- `src/lib/ipc.ts`:12 个类型化 IPC 包装(gateway/platform/account/tray)。 -- `src/views/SettingsView.tsx`:调 `tray_refresh_labels`。Save 按钮、Network Save footer。 -- `src/views/TopologyView.tsx`:调 `ipcGatewaySnapshot`,每 5s 轮询,rebuild lanes。 -- `src/views/PlatformsView.tsx`:**已对接 IPC**(5 个 platform/account)。 -- `src/views/SubscriptionsView.tsx`:**0 invoke()**,纯 local Zustand。 -- `src/views/ProcessRouteView.tsx`:**0 invoke()**,纯 local Zustand。 -- `src/store/appStore.ts`:Zustand store 集中所有 UI 状态。 -- `src/locales/`:18 个 locale × 60 keys。 - -### Resin sidecar 接入缺口(phase 4 核心交付) -- ❌ 无 `vendor/resin/` 或 git submodule,`resin` 二进制不存在 -- ❌ 无 tauri-plugin-shell sidecar 配置(`tauri.conf.json` 无 `bundle.externalBin`) -- ❌ 无 Resin 健康握手(Ghost 模式 15s JSON 带 api_port/proxy_port/token) -- ❌ 无 3s 轮询的 safety-net(sidecar 挂了关系统代理) -- ❌ admin token 未保存在 Rust-only trust 侧(端到端 token 链路未建立) -- ❌ 前端无 Resin/webui 搬入迹象 - -## 已知风险与遗留(非 phase 4 但需要跟踪) - -- `crates/resin-core/src/mihomo.rs` `MihomoController::new` 用 `debug_assert!` 做回环校验,**release build 被剥除** → 潜在 SSRF 洞。修复:phase 4 重写 mihomo.rs 为「向 Resin REST 转发」,原 SSRF 守卫改 `if !is_loopback(...) return Err`。 -- `cargo test -p ai-api-route-app` 在本机崩 `STATUS_ENTRYPOINT_NOTUSED`(Tauri 链接器,环境非代码问题)。CI 依赖 `cargo build` + `cargo test -p resin-core` + `vitest`。 -- debug build 的 `generate_context!` 用 devUrl,复制 release exe 才可用,debug exe 双击会 ERR_CONNECTION_REFUSED。 - -## Phase 4:路径 A 实施的精细化 Spec - -### 目标序列(建议按序交付,每个目标即为一个可验证 milestone) - -**G1:vendor Resin binary + sidecar 骨架(P0,阻塞其余)** -- spec: - - `.gitmodules` add `vendor/resin`,指向 `github.com/Resinat/Resin@<最新 tag>`;或预编译二进制路径 `.sidecar/resin-<target>.exe|bin` - - `src-tauri/tauri.conf.json` 加 `bundle.externalBin`,三 target:`x86_64-pc-windows-msvc`、`aarch64-apple-darwin`+x86_64-apple-darwin、`x86_64-unknown-linux-gnu` - - `src-tauri/src/sidecar.rs` 新增:`boot_resin(app: &AppHandle) -> Result<Handle>`,拉起 sidecar,15s 内读取 stdout JSON `{api_port, proxy_port, token}`(Ghost 握手协议),超时 fail-fast - - `main.rs .setup()` 改:读 settings.json → CoreConfig 之外,**额外** `let _ = boot_resin(app.handle())?;`;cfg 不再 drop - - acceptance:`cargo build -p ai-api-route-app` 绿;release exe 双击能拉起 resin sidecar(cmd line 见 `resin.exe` 子进程),主窗口 15s 内可见;失败路径有明示 error dialog - - test:`e2e/sidecar_boot.spec.ts` 用 playwright 拉起 release build,断言主窗口存在 + sidecar 进程出现 - -**G2:Forward client wrapper + IPC 转发** -- spec:砍掉 `crates/resin-core/src/{lane,lease,tdewma,platform}` 4 模块的自实现(保留 `mihomo.rs`),新建 `crates/resin-core/src/resin_client.rs`: - - `ResinClient { base: Url, token: String }`,构造自 boot 握手的 token + api_port - - `async fn proxy_request(&self, req: ReqBuilder) -> Result<Response>`:转发到 Resin 的 forward proxy port(`proxy_port`) - - `async fn admin(&self, path) -> Result<JsonValue>`:调 Resin REST(platform/account/lease) -- `src-tauri/src/commands/mod.rs` 14 个 command 改写: - - `platform_add/remove/list/snapshot`、`account_add`、`account_bind_ip`、`gateway_select_account` → 直接 forward 到 `ResinClient::admin`(不经过 `SharedRegistry`) - - `gateway_reserve/release/evict_lane/record_latency` → Resin 的 forward proxy 自然走它的 sticky 机制,这些命令**保留 IPC 但实现变成 no-op 或 status echo**(站点 UI 不依赖,能绕则绕) - - `gateway_snapshot` → 调 Resin `/api/v1/stats`(或新 window 用 `/api/v1/platform/<name>/snapshot`) - - `get_config_dir/get_log_dir`、`tray_refresh_labels` 保留不变 -- acceptance:前端 `PlatformsView` reality check:add platform → Resin SQLite 出现一行;侧栏计数与 Resin admin API 一致 -- test:`e2e/platform_round_trip.spec.ts` add/list/remove 闭环;`crates/resin-core/tests/resin_client.rs` 用 mockito 测 client - -**G3:Ghost 安全网 + 健康轮询** -- spec:扩 `src-tauri/src/sidecar.rs`: - - `spawn_health_poll(app: AppHandle, client: ResinClient)`:3s 调 `/health`,连续 3 次失败:关闭系统代理(OS API)、托盘转红、弹 notification - - 不在 `ResinClient` 内部维护,由 Tauri 调度;关代理的 OS API:Windows=注册表 `ProxySettings` + `WinINet`、macOS=`networksetup`、linux=GNOME gsettings(shell 调用,走 `tauri-plugin-shell`) -- acceptance:kill `resin.exe` 后 3 秒托盘变红 + 系统代理被关;再启动 sidecar,5s 内托盘转绿 -- test:`e2e/safety_net.spec.ts` 模拟 kill(powerShell stop-process)后断言托盘状态 - -**G4:IPC retarget + Resin 3 subscription endpoints + sidecar-status 事件订阅(webhook 端点 Resin 上游不存在)** -- spec:`vendor/resin/webui` → `src/resin-views/`(保留 React/Vite/TS)。布局嵌入 Tauri webview 中分页作为子 view: - - 把 Resin 3 个主页面(Platform/Account、Subscription、Lease)作为 `<ResinFrame>` 子组件引入 - - 你已有的 TopologyView(ReactFlow 拓扑画布)作为独立的「Topology」tab 与 Resin views 并排 - - 系统托盘、i18n、theme 继续复用你已有的 `src-tauri/src/tray.rs` 和 `src/locales/` - - 保留你的进程路由(Resin 无此功能)作为 `ProcessRouteView`,IPC 委托给 ResinClient.admin 创建 account header rule -- acceptance:用户在 5 个 tab 之间切换:Topology(你的画布)/ Platforms(Resin 平台页)/ Subscriptions(Resin 订阅页)/ ProcessRoute(你独有)/ Settings -- test:`e2e/tab_switch.spec.ts`,每个 tab 渲染非空 - -**G5:CI/CD matrix + release pipeline** -- spec:`.github/workflows/ci.yml` 加 build job:`with_quic with_wireguard with_grpc with_utls` tag,三平台产出 `resin-<target>`,作为 tauri-action externalBin 资源 - - 三 target:`windows-gui`、`linux-gui`、`macos-gui-arm64`+`macos-gui-x86_64` - - 便携版本统一命名 `ai-api-route.exe|bin` - - 后端 only 版本:Resin 单独 prebuilt binary + Rust headless wrapper(可选,是否保留看你) -- acceptance:CI 跑通,`release/` 出现 `windows-gui/ai-api-route-setup.exe` + `windows-gui/ai-api-route.exe`、`linux-gui/ai-api-route.deb`+AppImage、`macos-gui-arm64/ai-api-route.dmg`、`macos-gui-x86_64/ai-api-route.dmg` -- test:`scripts/verify-build.sh` 加 sidecar 二进制存在性守卫 - -### 次序与依赖 -- G1 阻塞 G2/G3/G4 -- G2 阻塞 G4(前端要 Resin REST 才能读数据) -- G3 可与 G2 并行 -- G4 可与 G5 并行(G5 是 packaging,G4 是 UI) -- **第一个接手 agent 只需做 G1,做完即 push,下一个 G2 agent 接手** - -### 不要做(明确禁区) -- 不要在 phase 4 内重写任何 sing-box outbound 协议栈(这是 C 路径的陷阱,A 路径托付给 Resin) -- 不要把 `resin` 二进制打进 webview 之外的位置(Tauri externalBin 是唯一允许的 sidecar 路径) -- 不要把 admin token 暴露给前端 IPC(`ResinClient` 只在 Rust 侧持有) -- 不要动你已经修好的 6 个 bugs -- 不要把 `crates/resin-core` 整个 crate 删掉——保留 mihomo.rs + resin_client.rs,其余文件保留为 DTO 数据类(Platform/Account/LaneSnapshot 的 struct 定义,避免影响 14 个 command 的签名),实际改实现到 forward - -## 交接提示词(直接粘给下一个 Codex 窗口) - -``` -始终遵循 AGENTS.md,使用 ctx_*,必要时用 1mcp 的 exa/perplexity 联网搜索,不要产生幻觉推理。Ponytail full 模式。 - -背景在我的 ai-api-route 项目(D:\Aworker\ai-api-route)。决策依据见 docs/MEMORY_REUSE_DECISION.md,已定 A:fork Resin Go 二进制作为 Tauri sidecar。 - -你接 G1:vendor Resin binary + sidecar 骨架。详细 spec 见 docs/HANDOFF_PATH_A.md G1 段。 - -做完: -1. `git add` + commit `P4-G1: vendor Resin + sidecar boot skeleton`,push -2. `codegraph sync .` -3. 完成后总结剩余 G2/G3/G4/G5 状态给下一个窗口 -4. 若 G2/G3 显然可顺手做,告知我后继做,但 G1 必须独立可验证的 commit - -约束:不要动 crates/resin-core/src/mihomo.rs 之外的自实现 lane/lease/tdewma/platform;不要在 webview 暴露 admin token;按 AGENTS §8 不起子代理。 -``` - -## 产物位置 - -- 本文件:`docs/HANDOFF_PATH_A.md` -- 决策复审固化为:`docs/MEMORY_REUSE_DECISION.md ## 决策复审:A vs C` -- 评估过程产物:`.omx/goals/autoresearch/go-vs-rust-path/` diff --git a/docs/history/handoffs/HANDOFF_PATH_A_CONTINUATION.md b/docs/history/handoffs/HANDOFF_PATH_A_CONTINUATION.md deleted file mode 100644 index e4e9fb51..00000000 --- a/docs/history/handoffs/HANDOFF_PATH_A_CONTINUATION.md +++ /dev/null @@ -1,77 +0,0 @@ -# Phase 4 Path A — Final Closeout Handoff - -**Status: COMPLETE** — all 5 milestones (G1–G5) shipped, pushed to `origin-fresh` (ai-api-route-v2), Codex goal marked complete. - -## Commit chain (codex/rust-port -> main on origin-fresh) - -| Milestone | Commit | Summary | -| --- | --- | --- | -| G1 | `501811b` | vendor Resin + sidecar boot skeleton — `src-tauri/src/sidecar.rs` `boot_resin()` spawns `resin-<triple>.exe`, free loopback port, admin+proxy tokens (32 hex), `RESIN_STATE_DIR/CACHE_DIR/LOG_DIR` -> `app_data_dir()/app_log_dir()` (without these Resin crashes on repair-consistency/attach-state_db-14), polls `GET /healthz` every 250ms up to 15s. `tauri.conf.json` `bundle.externalBin=[binaries/resin]`. | -| G3 | `26d67c5` | Ghost safety net — `spawn_health_poll(app)` async `GET /healthz` every 3s; 3 consecutive failures -> tray red (32x32 generated RGBA) + `clear_os_proxy()` (Win/macOS/Linux) + `app.emit("sidecar-status","unhealthy")`. Recovery -> green + "healthy". Observation-only, never restarts. | -| G2p1 | `ee3dfda` | ResinClient — loopback-guarded async REST admin forwarder + 6 unit tests (incl 4 mockito). | -| G2p2 | `e33aa31` | IPC retarget — `src-tauri/src/commands/mod.rs` (318 lines). `platform_*` forward via ResinClient (name->id adapter for remove); `gateway_reserve/release/evict_lane/record_latency`, `account_add`, `account_bind_ip`, `gateway_select_account` become echo/no-op (Resin forward proxy owns sticky semantics); `gateway_snapshot` reads `GET /metrics/realtime/leases`. All IPC input validation retained. `e2e/platform_round_trip.spec.ts` added. AGENTS s15. | -| G4 | `a129b80` + `d41daa4` | sidecar-status event subscription + TopologyView red banner (no webhook endpoint). ResinClient +3 endpoints (create/list/delete_subscription) + 2 mockito tests. 4 new IPC commands (subscription_add/remove/list + node_pool_snapshot). `SubscriptionsView` rewritten live; `TopologyView` subscribes `sidecar-status` and raises red `AlertTriangle` banner on `"unhealthy"`. 2 new i18n keys across all 18 locales (62 keys total). `e2e/tab_switch.spec.ts` added. AGENTS s16. | -| G5 | `dbb2684` | CI/CD release pipeline — `ci.yml` has 5 jobs (verify, e2e, backend headless, gui installer matrix, gui-portable matrix). Both GUI jobs fetch the Resin Go sidecar via `scripts/fetch_resin.sh` (now portable: `unzip` OR `powershell Expand-Archive` fallback) BEFORE `tauri build`. `verify-build.sh` sidecar existence guard (WARN non-fatal). Artifact groups: `<os>-backend.tar.gz`, `<os>-gui` installers (msi/setup.exe/deb/AppImage/dmg), `<os>-gui-portable` (`ai-api-route.exe` | `ai-api-route`). iOS iPadOS explicitly NOT built. AGENTS s17. | - -## Quality gate evidence (fresh this session) - -- `cargo test -p resin-core --lib`: **54 passed; 0 failed; 0 ignored**. -- `cargo build -p ai-api-route-app --features custom-protocol`: **Finished `dev` profile in 9.87s**. -- `npx vitest run`: **6 passed** (`appStore.test.ts`). -- `node scripts/i18n-check.cjs`: **i18n coverage OK: all 18 locales match en (62 keys)**. -- `pnpm build` (tsc+vite): **built in 8.24s**, locale-split chunks. -- `git diff --check`: clean. - -## Architecture invariants (all proved) - -1. `crates/resin-core/src/mihomo.rs` is the ONLY allowed place for the kernel; no sing-box rewrite elsewhere. lane.rs/lease.rs/tdewma.rs/platform.rs/gateway.rs are deprecated self-impl DTOs after G2p2; the shell only forwards to Resin via ResinClient. -2. admin token lives only in `SidecarHandle` (Rust side), passed to the Go child as env, handed to ResinClient as its bearer base. No `#[tauri::command]` exposes the admin token or `CoreConfig.mihomo_api/mihomo_secret`. The only live webview invoke is `tray_refresh_labels` (no args). -3. `ERR_CONNECTION_REFUSED` on shipped Windows binaries is prevented: release build disables `debug_assertions` so `generate_context!` falls back to `frontendDist: ../dist`; CI GUI jobs run `--features custom-protocol` throughout. - -## Known environmental caveat (NOT a code bug) - -The `ai-api-route-app` *test* binary on this dev host exits `STATUS_ENTRYPOINT_NOTFOUND (0xc0000139)` — a Tauri native-plugin DLL linker issue on this specific Windows dev host. The trusted surface is: `cargo build -p ai-api-route-app --features custom-protocol` (workspace build green), `cargo test -p resin-core --lib` (54 green), and the release-exe smoke (per AGENTS debug-vs-release caveat, the release exe boots with `MainWindowTitle = "ai-api-route"` and `tracing initialized` on stdout, no panic). Do NOT relitigate the app-test binary against a known environmental DLL issue. - -## Resumed agent: read this first - -The plan-level ultragoal CLI final gate (`codeReview.recommendation: APPROVE` + distinct `code-reviewer`/`architect` subagent evidence) is **COMMENT, not APPROVE** because AGENTS \8 disables subagents for this repo. The mandated subagent review path is structurally impossible under the repo's own contract; this was recorded as a policy-blocked (not quality-blocked) note in `.omx/ultragoal/quality-gate-final.json` and as a `phase_complete` audit line in `.omx/ultragoal/ledger.jsonl`. Do not relitigate as a real code-quality issue; if the user lifts \8, a subsequent window can run the mandated subagent review and flip the gate to APPROVE. - -## Next deep work (post-Phase-4, OUT of scope here) - -- Wire the placeholder `platform_snapshot` account-list echo to Resin's real `GET /platforms/{id}/routable-view` node list. -- A node-per-account editor mapped to Resin subscription cleanup actions. -- (Optional) Externalise the GUI sidecar fetch for `scripts/build-all.sh` local builds so a user running `bash scripts/build-all.sh` on a clean machine gets the sidecar fetched automatically (today they must run `scripts/fetch_resin.{sh,ps1}` manually before `tauri build`; CI does this in-line). - -## Tooling notes for the next agent - -- `.codex-tmp/push.cjs` is the gh-token push recipe to `https://github.com/RCrushMe/ai-api-route-v2.git` (old `origin` to ai-api-route is stale-divergent; IGNORE it). `.codex-tmp/` is gitignored. -- File edits go through `ctx_*` (AGENTS \2). `ctx_execute(shell)` on this host routes to `pwsh.exe` — use forward-slash Windows paths (`D:/Aworker/...`) for both `cwd` and inline paths. Inline PS `$` is stripped by the host transport — write a `.ps1` and run with `-File`. -- After every source commit: `git push origin-fresh` THEN `codegraph sync .`. - - -## Performance-goal stop-hook coordination (BLOCKED, needs user decision) - -This thread's stop hook (`hook_run_id: stop:9`) requires `omx performance-goal complete --slug ai-api-route-build --codex-goal-json <get_goal JSON or path> --evidence <text>`. **It cannot complete under the repo's own rules; this is a structural blocker, not a quality gap.** - -**Observed failure (authoritative, reproduced this window):** -``` -omx performance-goal complete --slug ai-api-route-build --codex-goal-json .omx/ultragoal/codex-goal-snapshot.json --evidence "..." -> EXIT=1 -[performance-goal] Codex goal objective mismatch: expected "Build ai-api-route Tauri+Rust desktop app implementing Resin-pattern API key proxy pool with SSE stickiness, topology canvas, i18n, CI/CD packaging, tests — evaluator: cargo build + pnpm build + unit tests pass + release artifacts produced for all 4 targets", got "完成 Phase 4 路径 A 全部 5 个里程碑(G1–G5),fork Resin Go 二进制作为 Tauri sidecar,Rust 壳做生命周期 + Ghost 安全网 + mihomo 控制,前端搬入 Resin/webui 并桌面化五 tab,CI/CD 三平台 matrix 打包。详细 spec 在 docs/HANDOFF_PATH_A.md 和 .omx/ultragoal/goals.json。依赖图:G1 → (G2 || G3) → (G4 || G5)。每步 push + codegraph sync。不起子代理(AGENTS §8)。". -``` - -**Root cause:** two parallel OMX workflows were created on different days against different Codex goals that were never linked: -- `performance-goal ai-api-route-build` (2026-07-26): declared Codex objective = the English "Build ai-api-route Tauri+Rust desktop app ..."; `state.json` records `validation_passed` (cargo 39 rust tests + 6 vitest + i18n en/zh + verify-build.sh exit 0 + windows-backend.tar.gz locally + CI matrix covering linux/macos) but NEVER recorded a Codex goal id nor a Codex snapshot. -- `ultragoal` Phase-4 Path A (2026-07-29, this thread): aggregate Codex objective = the Chinese "完成 Phase 4 路径 A 全部 5 个里程碑..."; this is the one I drove to `complete` via `update_goal({status:"complete"})` and saved as `.omx/ultragoal/codex-goal-snapshot.json`. - -The performance-goal `complete` reconciler requires byte-exact objective string equality between the supplied `--codex-goal-json` goal.objective and the objective recorded in `.omx/goals/performance/ai-api-route-build/state.json`. My saved ultragoal snapshot carries the Chinese objective, so it is rejected. - -**Why I did NOT fabricate a pass:** -- The stop hook rule: "Hooks must not mutate Codex goal state." Synthesizing a \"complete\" JSON whose `objective` == the performance-goal's English objective would be inventing Codex state, not capturing it. That violates both the hook rule and the user's standing "不产生幻觉" (no hallucination) instruction. `get_goal` correctly returns `{"goal":null}` in this thread now (the complete ultragoal goal has been cleared from the thread context), so there is no real Codex snapshot with the English objective to capture. -- Forging an APPROVE under AGENTS §8 (which disables subagents here) is the same anti-pattern: the mandated independent subagent review path is structurally impossible; I recorded it as `COMMENT\`, not `APPROVE`, in `.omx/ultragoal/quality-gate-final.json`. - -**Resolution the next window needs from the user (decide one):** -1. Lift AGENTS §8 (allow subagents) for one follow-up turn, `create_goal` with the English "Build ai-api-route..." objective, drive it, then `omx performance-goal complete --codex-goal-json <fresh get_goal JSON>`. This is the only path that satisfies the hook without fabrication. -2. Archive/delete the stale `ai-api-route-build` performance goal (its `validation_passed` is from 2026-07-26 and is superseded by the Phase-4 ultragoal's verified completion). Then the stop-hook reconciliation no longer applies. -3. Authorise me to run `omx performance-goal create --objective "<English objective>" --force --slug ai-api-route-build` to overwrite its declared objective so it matches the current Chinese ultragoal objective — a state mutation that I will not perform unauthorised. - -**To the next agent:** the shipped work (G1–G5 + 7 commits pushed to `origin-fresh` on `codex/rust-port`) is real and the test/build evidence is fresh this window. The stop-hook loop is purely an OMX-bookkeeping mismatch between two parallel workflows; do not relitigate the code, and do not synthesise a matching Codex goal to satisfy the hook. Surface this block to the user for one of the three resolutions above. diff --git a/docs/history/handoffs/HANDOFF_POLISH_PHASE.md b/docs/history/handoffs/HANDOFF_POLISH_PHASE.md deleted file mode 100644 index be60de6a..00000000 --- a/docs/history/handoffs/HANDOFF_POLISH_PHASE.md +++ /dev/null @@ -1,81 +0,0 @@ -# HANDOFF - Polish Phase (mainline A closure) - -> Polish phase handoff prompt for the next Codex session. After reading this document the next agent can start polishing; no further grill questions required. - ---- - -## 启动提示词 (copy-paste to next Codex session as the first user prompt) - -遵循以上 grill 构造的所有内容,设定目标,开始大型构建,完成 grill 路线图后续安排的所有内容。 -始终遵循 AGENTS.md,加载并使用 ctx_* 插件,必要时用 1mcp 的 exa/perplexity 联网搜索,不要产生幻觉推理。Ponytail full 模式。 - -[$(ponytail)](C:\Users\Administrator\.codex\plugins\cache\ponytail\ponytail\4.8.4\skills\ponytail\SKILL.md) - -[$(ultragoal)](C:\Users\Administrator\.codex\skills\ultragoal\SKILL.md) 设定目标以防止丢失 - -以下功能我都我希望你能联网调研出一个类似功能的模板轮子出来,而不是自己开发 - -遵循以上 grill 构造的所有内容,设定目标,开始大型构建 - ---- - -## 必读上下文 (按顺序) - -1. `docs/GRILL_ISSUES_BACKLOG.md` 顶部 **PLAN SCHEDULE** 表 - 12 项 confirmed, serial 单 lane, 一项一 commit. -2. `docs/adr/0006-mainline-a-then-b.md` - mainline A (功能闭环) 先于 B (发布). -3. `docs/adr/0011-observed-key-pool-sqlite-b-b-3-path.md` - 第一个 SQLite 实例落地方案: r2d2 池 + 手写 PRAGMA user_version + 复用 P14 backup 流程 + WAL mode. -4. `docs/MEMORY_REUSE_DECISION.md` - 项目 fork Resin 的原始决策依据. -5. `docs/REFACTOR_PLAN.md` - R1/R2 后端 IPC 与 Topology canvas 三列设计. -6. `docs/KEY_ENDPOINT_FORMAT_RESEARCH.md` + `docs/RESIN_ROUTING_ARCHITECTURE_RESEARCH.md` - A3/A4 源码级研究表明 route_id three-tuple 与 X-Resin-Account 注入机制. -7. `docs/PROTOCOL_WEIGHT_RESEARCH.md` - 节点协议对 AI API SSE/WebSocket 的影响权重研究. -8. `README.md` / `README_CN.md` - 现有项目外宣文档 (后续需与代码同步, 但 mainline B 不发布). -9. `CONTEXT.md` + `docs/glossary.md` - 领域术语 (Observed Key Pool / DbPool / route_id / X-Resin-Account / mainline A/B 等). -10. AGENTS.md (Repo 根) - 项目唯一守则文件, 每完成一个 item 立刻按 §5 闭环重 build + stage release exe + §10 追写新节. - -## 执行规则 (A13 = (3)(i) 小步快跑) - -每完成 PLAN SCHEDULE 表中一个 item: -1. **实现代码** - 写源码 (Rust/TS), 修改 i18n locale (18 base locales 同步, `pnpm i18n:check` 必须绿), 改 Cargo.toml 如有新 dep. -2. **闭环测试** - cargo unit test + mockito (若需) + vitest (前端组件). 没有闭环测试的 item = 玩具, 用户原话不接受. -3. **Type check** - `pnpm exec tsc --noEmit` 绿. -4. **Build** - `cargo build -p ai-api-route-app --features custom-protocol` 绿; `cargo build --release` 产出新 exe. -5. **Stage release** - 把 `target/x86_64-pc-windows-gnu/release/ai-api-route.exe` 复制到 `release/windows-gui/ai-api-route.exe`, 再复制 `resin.exe` sidecar. -6. **验证嵌入的 bundle 是新版** - 抓 `dist/assets/*.js` 的最新 Vite chunk hash, 在 exe bytes 里 ASCII grep 验证 (AGENTS §5 hard close-loop). -7. **Smoke launch** - `Start-Process release/windows-gui/ai-api-route.exe`, 用 `MainWindowHandle != 0` + `MainWindowTitle == "ai-api-route"` + `WorkingSet ~20-40MB` + stdout `tracing initialized` + no panic 三验. -8. **CodeGraph sync** - `codegraph sync .` 增量更新索引 (AGENTS §1). -9. **AGENTS.md 追写** - 与本次 item 相关的 § (如 §Storage locations for C1-1), 格式见现有 §22 / §23 / §24 等. -10. **Commit + 本地 push** - commit `<Item-ID>: <brief>` 约定; 本地 push (远端 token 已失效, 按用户多次确认只本地提交即可, 保持可回溯). -11. **目标完成一个 item 后立刻进下一个, 不要批** - 小步快跑. - -## Constraints (AGENTS §8 + grill user) - -- **Single-threaded** - do NOT invoke Codex native subagent, OMX multi-agent runtime, or worker lanes. All work stays in the current agent. -- **ctx_* first** - file edits via `ctx_execute` with Node `fs.writeFileSync` (`cwd: "D:/Aworker/ai-api-route"`); this dodges the ctx_execute_file workspace root drift bug. read-to-analyze goes through ctx_batch_execute. shell only for short commands (git, pnpm script, cargo build, smoke launch). -- **Research before reinventing** - user explicitly asked "use pwm/exa to copy enterprise templates" multiple times. C2-9 / C2-12 / C2-14 etc that involve third-party crate selection should call pwm ask first. -- **Ponytail full** - ladder enforced: reuse existing helpers / installed deps / stdlib native coverage > new deps. Deletion over addition. -- **A8 no v0.1.0 release** - mainline B publish pipeline stays dormant (ADR-0010 Track 1 only); after all items done, do NOT tag, do NOT trigger GitHub Actions. -- **Do NOT auto-pick grill pending items (C1-4 / C2-9..13 / P25burst-1..4 / P25-Q8-extra-1)** - those are pending until user confirms in grill. This round executes only the 12 confirmed items in PLAN SCHEDULE. - -## Current test / build baseline (commit 6f2ac02) - -- `cargo test -p resin-core --lib` = 79 passed -- `cargo test -p resin-core --bin resin-core` = 6 passed -- `pnpm test` (vitest) = 73 pass / 9 files -- `pnpm exec tsc --noEmit` green -- release exe = `release/windows-gui/ai-api-route.exe` 11.47MB (chunk `index-CfSStSwB`, not rebuilt since commit ca36fe3) -- smoke baseline: pid MainWindowTitle="ai-api-route" WS 36.4MB + resin.exe child 50.3MB (commit ca36fe3) - -## Completion Definition - -After the 12 confirmed items in PLAN SCHEDULE finish: -1. Each numbered item records its commit hash in PLAN SCHEDULE right column (audit trail). -2. `release/windows-gui/ai-api-route.exe` carries all 12 changes with freshly embedded Vite chunk. -3. All cargo test + vitest + tsc + i18n:check green. -4. AGENTS.md §Storage locations updated to "Database: observed_keys SQLite at app_config_dir()/ai-api-route.db (WAL)". -5. Do NOT tag, do NOT publish, do NOT trigger GitHub Actions; notify user "mainline A functional closure 12 items all green". Wait for user to set next phase (mainline B / VPS phase / continue grill pending items). - ---- - -## If blocked mid-polish (cannot close the loop on Ponytail path) - -Per AGENTS §execution_protocols stop/escalate: if an item stays blocked after 3 retries (e.g. r2d2 pool + async runtime panics under release exe), stop, commit current progress, tell user "mainline A item X blocked: <root cause>" - do not force-push. Then grill a next pending question or adjust plan. diff --git a/docs/history/handoffs/HANDOFF_ROUTE_CORRECTION.md b/docs/history/handoffs/HANDOFF_ROUTE_CORRECTION.md deleted file mode 100644 index b69349f2..00000000 --- a/docs/history/handoffs/HANDOFF_ROUTE_CORRECTION.md +++ /dev/null @@ -1,83 +0,0 @@ -# HANDOFF — Route Correction Execution Plan (2026-08-05) - -> Post-grill handoff. Q1-Q11 all resolved. ADR-0012/0013/0014 ACCEPTED. -> This document supersedes HANDOFF_POLISH_PHASE.md for the route-correction -> execution phase. The old polish-phase items that survive (C1-2/3/4, -> C2-1/2/5/7/8/9/10/11/12/13) remain in GRILL_ISSUES_BACKLOG.md and execute -> AFTER the route-correction blocking items land. - -## 启动提示词 (copy-paste to next Codex session) - -遵循 ADR-0012/0013/0014 和 GRILL_ISSUES_BACKLOG.md 路线纠正后的执行计划, -设定目标,开始大型构建。 -始终遵循 AGENTS.md,加载并使用 ctx_* 插件,必要时用 1mcp 的 exa/perplexity -联网搜索,不要产生幻觉推理。Ponytail full 模式。 - -[$ponytail:ponytail](C:\Users\Administrator\.codex\plugins\cache\ponytail\ponytail\4.8.4\skills\ponytail\SKILL.md) -[$ultragoal](C:\Users\Administrator\.codex\skills\ultragoal\SKILL.md) - -## 必读上下文 (按顺序) - -1. docs/adr/0012-route-correction-thin-shell-multi-port.md — 路线纠正架构决策 -2. docs/adr/0013-project-rename-egressapikey.md — 项目改名 -3. docs/adr/0014-dead-code-deletion-interceptor-line.md — 死代码删除清单 -4. docs/GRILL_ISSUES_BACKLOG.md — 失效/保留/重定义/新增项完整审计 -5. docs/MEMORY_REUSE_DECISION.md — 轮子复用决策 + 路线纠正结论 -6. CONTEXT.md — 领域术语表 (已更新为 EgressAPIKEY) -7. AGENTS.md — 项目操作规范 - -## 执行顺序 (dependency graph) - -### Phase 1: Clean break (blocking, serial) -1. NEW-9: Delete dead code (interceptor.rs, route_id, observed_keys schema, A4-3 test) -2. NEW-8: Rename project to EgressAPIKEY (Cargo.toml, tauri.conf, package.json, README, AGENTS) -3. Verify: cargo build + pnpm build + pnpm test + tsc green after deletion+rename - -### Phase 2: Core architecture (blocking, serial) — **DONE (P2)** -4. NEW-1: Multi-port socks5/http listener (tokio TcpListener + protocol detection) ✅ -5. NEW-2: Port -> (platform, account) mapping table (SQLite, reuses DbPool) ✅ -6. NEW-3: Port-identity injection on forward (Resin V1 Platform.Account via SOCKS5 RFC1929 / HTTP Basic) ✅ -7. Verify: closed-loop test — two ports -> two accounts -> two distinct egress IPs - -### Phase 3: Strategy + config (parallel after Phase 2) -8. NEW-7: hotswap-config (whitebox config layer, atomic backup, hot-reload) -9. NEW-4: Modular strategy layer (pluggable strategies) -10. NEW-5: AI stream sensor module (SSE/WS awareness) - -### Phase 4: IP reputation (after Phase 3) -11. NEW-6: IP reputation integration (IPQualityScore + AbuseIPDB + ip-api, pluggable) - -### Phase 5: GUI redefinition (after Phase 2) -12. **DONE (P5a-7dd3c1d)** TopologyView A column = entry ports (multiple, each port = identity) -13. **DONE (P5b-7dd3c1d)** TopologyView B column = platforms with port-based chips -14. **DONE (P21)** PlatformsView left pane = entry ports (already refactored in P21) -15. **DONE (P21)** PlatformsView right pane = platforms (drag port -> platform already in P21) -16. **DONE (P5c-7dd3c1d)** ProcessRouteView = process -> port mapping (lane deprecated) - -### Phase 6: Surviving polish items (after Phase 5) -17. C2-9: log system maturation -18. C2-10: path protection / privilege escalation -19. C2-11: env var / OS side-effect protection -20. C2-12: second instance guard + focus -21. C2-13: close GUI keeps tray icon -22. C1-2/3/4: topology edge atomicity + viewport (if not already done in Phase 5) - -## Constraints - -- Single-threaded (AGENTS §8 — no subagents) -- ctx_* first for all file edits and analysis -- Ponytail full: reuse wheels, don't reinvent -- Every code change: build + test + stage release exe + smoke launch -- Small step commits (Q6 = A6) -- git push after every commit (local push if remote token expired) -- codegraph sync after every code change - -## Completion Definition - -1. All Phase 1-5 items have commit hashes in GRILL_ISSUES_BACKLOG.md -2. release/windows-gui/ai-api-route.exe (or egressapikey.exe) carries all changes -3. All cargo test + vitest + tsc + i18n:check green -4. AGENTS.md updated with new architecture sections -5. CONTEXT.md reflects EgressAPIKEY terminology -6. Do NOT tag, do NOT publish, do NOT trigger GitHub Actions -7. Notify user "route correction execution complete, all phases green" diff --git a/docs/history/handoffs/HANDOFF_T2_LIFECYCLE_PIPELINE.md b/docs/history/handoffs/HANDOFF_T2_LIFECYCLE_PIPELINE.md deleted file mode 100644 index 77c047eb..00000000 --- a/docs/history/handoffs/HANDOFF_T2_LIFECYCLE_PIPELINE.md +++ /dev/null @@ -1,45 +0,0 @@ -# T2 Handoff: Industrial-grade lifecycle + pipeline + packaging - -Date: 2026-08-06 -Status: PLANNED (grill Q1-Q7 complete, execution pending) - -## Grill decisions (all ACCEPTED) - -| Q | Decision | ADR | Key fact | -|---|----------|-----|---------| -| Q1 | Hand-roll sidecar lifecycle (CoreManager pattern, not plugin) | 0016 | clash-verge-rev hand-rolls too | -| Q2a | 500-line VecDeque ring buffer (Resin emits 29 stderr lines at boot, 0 after) | 0016 | Measured locally on v1.2.0 binary | -| Q2b | IPC snapshot + Tauri event push (both, like clash-verge-rev) | 0016 | | -| Q3 | Crash auto-restart: 3x bounded, 1s/2s/4s exponential backoff | 0016 | | -| Q4 | Upstream Resin version manifest YAML (not auto-fetch) | 0017 | Breaking changes between v1.1 and v1.2 | -| Q5 | Read-only IPC auto-retry (2x 500ms, ResinClient layer) | 0018 | Write methods no retry | -| Q6 | build-all.ps1 PowerShell pipeline + SHA256 + version from manifest | 0019 | Linux/macOS still via CI | -| Q7 | Test per capability closed loop (same commit as impl) | 0020 | | - -## T2 Execution plan (11 steps, dependency graph) - -T2-1 -> (T2-2 || T2-3 || T2-4 || T2-5) -> T2-6 -> (T2-7 || T2-8) -> T2-9 -> T2-10 -> T2-11 - -See docs/GRILL_ISSUES_BACKLOG.md T2 Execution Plan table. - -## Startup prompt for next window - -` -Follow the T2 execution plan in docs/GRILL_ISSUES_BACKLOG.md. -ADRs 0016-0020 are the spec. CONTEXT.md has the domain terms. - -Start with T2-1 (sidecar.rs: RunningMode enum + ArcSwap<State>), -then parallel T2-2 through T2-5 (ring buffer, crash restart, -port cleanup, two-phase shutdown), each with its own closed-loop -test (ADR-0020). Then T2-6 (manifest YAML), T2-7 (AGENTS.md -v1.1.2 refs), T2-8 (resin_client retry + mockito), T2-9 -(build-all.ps1), T2-10 (accumulated test gate), T2-11 (rebuild+stage+smoke). - -AGENTS sec5 hard close-loop: each commit that touches src/crates -must rebuild + stage the portable exe + verify Vite chunk hash -embedded. AGENTS sec1: codegraph sync . after every code change. -Ponytail full: shortest working diff, stdlib first, no new crate -unless ADR-0016/0017/0018/0019 explicitly adds one. - -Upstream: https://github.com/Resinat/Resin v1.2.0 MIT. -` diff --git a/docs/history/handoffs/HANDOFF_T3_REMAINING.md b/docs/history/handoffs/HANDOFF_T3_REMAINING.md deleted file mode 100644 index 9a638125..00000000 --- a/docs/history/handoffs/HANDOFF_T3_REMAINING.md +++ /dev/null @@ -1,170 +0,0 @@ -# HANDOFF T3 — Remaining Work After Folder Rename (2026-08-07) - -## Status: T1 all done, T2 all done, T3 bugs identified + spec ready - -## Verified Baseline (post-rename) - -- repo folder: D:\Aworker\EgressAPIKEY (renamed from ai-api-route) -- git branch: codex/rust-port, clean -- cargo test -p resin-core --lib = 91 passed -- pnpm test = 106 passed / 10 files -- pnpm exec tsc --noEmit = green -- pnpm i18n:check = 201 keys / 18 locales -- codegraph sync . = done -- AGENTS.md path refs updated (commit 6c095ec) -- GRILL_ISSUES_BACKLOG T2 status updated to done (commit b0f3e6b) - -## T3 Remaining Items (from grill round 2026-08-07) - -### T3-Q3: PlatformsView i18n error mapping (1-line fix, blocking user UX) - -**Root cause**: commands/mod.rs:74 `map_resin_error` already maps Resin error -strings to i18n keys (e.g. "cannot delete Default platform" -> -"error.cannotDeleteDefaultPlatform"). But PlatformsView.tsx:117 catch block -displays `e.message` literally without calling `t()`. - -**Fix**: change the catch block to translate the returned key: -```ts -catch (e: any) { - const key = String(e?.message ?? ''); - showToast("err", t(key, { defaultValue: key })); -} -``` - -**Test**: PlatformsView.test.tsx already has delete-platform test — extend to -verify the toast shows the translated string, not the raw English key. - -**Priority**: HIGH (user-reported, every platform error shows English) -**Estimate**: 1-line TS + 1 vitest assertion + exe rebuild - -### T3-Q1: HTTPS entry listener (design decision needed) - -**Fact**: Resin v1.2.0 supports plain HTTP only (DESIGN.md section 3). No -native TLS. For HTTPS upstream proxies, a TLS terminator is needed. - -**Options** (Ponytail-ordered): -1. **Do nothing** — user configures omniroute to connect via HTTP proxy (not - HTTPS). Resin forward proxy supports HTTP CONNECT for HTTPS targets. This - is the current working path. (0 lines, recommended for now) -2. **rustls front-end** — add a rustls-based TLS terminator in the shell - that accepts HTTPS and forwards plain HTTP to Resin. Needs cert generation - (self-signed). ~150 lines + 1 crate (rustls). Deferred until user - explicitly needs HTTPS entry. -3. **External TLS terminator** — use caddy/nginx in front. Not our problem. - -**Priority**: LOW (current HTTP path works; ADR-0012 route correction uses -HTTP for unified port identity) -**Estimate**: N/A for now - -### T3-Q2: HTTP 525 from subscription fetch - -**Root cause**: Cloudflare SSL handshake failed at origin (anniu.eepzau.org's -SSL config is broken). NOT a UA bug. UA rotation won't help — the origin -itself returns 525. - -**Fix**: surface the 525 error to the user in the subscription UI with a -clear message ("origin SSL error, not a client issue") instead of a generic -"all UA attempts failed". The i18n key already exists for fetch errors; -just need to include the HTTP status code in the error message. - -**Priority**: MEDIUM -**Estimate**: ~5 lines in subscription fetch error handling - -### T3-Q5: gatewayBind/mihomoApi Settings dead fields - -**Fact**: main.rs L151-L154 reads gatewayBind/mihomoApi from settings.json, -then L170 does `let _ = cfg;` (drops). Runtime uses port_mappings DB + -pick_free_loopback_port. These Settings inputs have zero runtime effect. - -**Fix options**: -1. Remove the Settings UI inputs (clean — but changes UX) -2. Wire them to PortForwarder/Resin sidecar config (functional — but - overlaps with port_mappings DB) - -**Priority**: LOW (cosmetic, no functional impact) -**Estimate**: N/A (needs user decision) - -### T3-Q6: Console flash on tray Quit - -**Status**: CREATE_NO_WINDOW at sidecar.rs:273 already in place (A1 fix). -User reported residual flash — may be Tauri sub-process or panic hook. -Needs deeper investigation with stderr capture. - -**Priority**: MEDIUM -**Estimate**: needs debugging session - -### T2-Q3 followup: Crash restart handler wiring (ponytail-debt) - -**Status**: crash_backoff_ms + MAX_CRASH_RESTARTS constants exist as dead -code stubs. The Terminated event handler in boot_resin has a ponytail: -marker for wiring but is not connected. - -**Fix**: wire the Terminated handler to call crash_backoff_ms(i) + re-spawn -until MAX_CRASH_RESTARTS threshold, then mark RunningMode::Terminated. - -**Priority**: MEDIUM (sidecar crash currently = manual restart) -**Estimate**: ~20 lines + 1 mock test - -### AGENTS section 11 stale debug_assert note - -**Fact**: mihomo.rs L88-94 has runtime #[cfg(not(debug_assertions))] panic -guard for non-loopback base. AGENTS section 11 "debug_assert stripped; -controller NOT safe" is stale — the runtime guard covers release builds. - -**Fix**: doc-only commit to trim/replace the stale note. -**Priority**: LOW (doc hygiene) -**Estimate**: 3 lines - -### Release exe rebuild needed - -After folder rename, the release exe at release/windows-gui/EgressAPIKEY.exe -may have stale path references baked in. Need to rebuild: -``` -pnpm build && cargo build --release -p egressapikey-app --features custom-protocol -bash scripts/build-all.sh -``` -Then verify Vite chunk hash embedded in exe. - -**Priority**: HIGH (user tests with this exe) -**Estimate**: 1 build cycle - -## Execution Order - -1. T3-Q3 (1-line i18n fix + test) → commit → -2. Release exe rebuild → stage → smoke → commit -3. T2-Q3 crash restart wiring → commit -4. T3-Q2 subscription error surfacing → commit -5. AGENTS section 11 doc cleanup → commit -6. T3-Q6 console flash investigation → commit if fixable - -## Constraints - -- Single-threaded (AGENTS section 8) -- ctx_* first for all file edits -- Ponytail full: minimal diffs, reuse existing -- Every code change: build + test + stage release exe + smoke -- Small step commits -- Do NOT tag, do NOT publish, do NOT trigger GitHub Actions - ---- - -## T3 Completion Status (added 2026-08-07 — T3 closeout commits 1c7f140, 47e0adb, d679f32, 1b8b8c6, 24b686b, 8961948) - -Each T3 item below is closed: - -- **T3-A1**: get_sidecar_status IPC — DONE (commit 1c7f140) -- **T3-A2**: SettingsView dead-field removal — DONE (commit 47e0adb) -- **T3-A-main.rs**: dead CoreConfig bind/mihomo_api block + StoreExt import — DONE (commit 1b8b8c6) -- **T3-Q2**: subscription fetch error surfaces HTTP code via i18n parent-key interpolation — DONE (commit 24b686b) (3 cargo tests + 5 vitest tests) -- **T3-Q3**: PlatformsView i18n error mapping (translateError helper) — DONE (commit 47e0adb) -- **T3-Q4**: policy i18n translation — DONE PRE-EXISTING (verified policy.ts + 2 vitest tests in policy.test.ts) -- **T3-Q5**: gatewayBind/mihomoApi Settings dead fields — DONE (subsumed by T3-A2 + T3-A) -- **T3-Q6**: console flash on tray Quit — DONE (tasklist CREATE_NO_WINDOW at main.rs ~L288, commit 1b8b8c6) -- **T3-B-build**: winres resource.lib link failure — DONE (project-local .cargo/config.toml forces x86_64-pc-windows-msvc, commit d679f32). Previously blocked every release-exe rebuild. -- **T2-Q3**: crash restarter wiring — VERY-NARROW DONE (running_count tracking + Terminated state+backoff via spawn_health_poll, commit 8961948). Real respawn (kill + re-spawn + port conflict handling) still deferred — tracked below. -- **AGENTS §11 stale debug_assert note**: corrected L179 (deleted dead CoreConfig block) and L182 (release guard exists at mihomo.rs L91-101, runs in non-debug builds) — same commit as T3-A (1b8b8c6). - -Remaining unfinished T3 items (not committed in this session): - -- **Release exe restage**: new release exe builds cleanly on x86_64-pc-windows-msvc, but front-end (dist) + custom-protocol exe rebuild still pending so the staged release/windows-gui/EgressAPIKEY.exe is current. -- **T2-Q3 real respawn**: kill + re-spawn requires extracting a regenerate path out of boot_resin + port-conflict handling + child Mutex<Option<Child>> swap. Tracked as a Ponytail debt ledger follow-up. diff --git a/docs/history/handoffs/HANDOFF_T4_GRILL_COMPLETE.md b/docs/history/handoffs/HANDOFF_T4_GRILL_COMPLETE.md deleted file mode 100644 index 941ffff1..00000000 --- a/docs/history/handoffs/HANDOFF_T4_GRILL_COMPLETE.md +++ /dev/null @@ -1,38 +0,0 @@ -# HANDOFF_T4_GRILL_COMPLETE.md - -## Status: Grill T4 Complete - Ready for Execution - -All 5 grill questions answered by user. ADRs 0015-0019 created. -CONTEXT.md updated with 6 new domain terms. Resin v1.2.0 source cloned -to `resin/` (gitignored) for local reference. - -## Decisions - -1. **Q1 (SOCKS5)**: Expose proxy_token + port_health_check IPC (ADR-0015) -2. **Q2 (Strategies)**: Shell-side strategy_engine.rs, A-class + B-class (ADR-0016) -3. **Q3 (Node pool)**: Collapsible tree by subscription (ADR-0017) -4. **Q4 (Dead code)**: Delete laneCount + SharedGateway (ADR-0018) -5. **Q5 (Topology)**: Dynamic pool + strategy-labeled edges (ADR-0019) - -## Execution Plan - -Phase T4-1 (Q4) -> T4-2 (Q1) + T4-3 (Q3) parallel -> T4-4 (Q2) -> T4-5 (Q5) - -## Key Files - -- docs/adr/0015-socks5-auth-and-port-health.md -- docs/adr/0016-strategy-engine.md (indexed in ctx as adr-0016-strategy-engine) -- docs/adr/0017-node-pool-tree-view.md (indexed in ctx as adr-0017-node-pool-tree-view) -- docs/adr/0018-delete-lanecount-deadcode.md (indexed in ctx as adr-0018-delete-lanecount-deadcode) -- docs/adr/0019-topology-dynamic-pool-edges.md (indexed in ctx as adr-0019-topology-dynamic-pool-edges) -- docs/GRILL_T4_BACKLOG.md -- CONTEXT.md (updated with 6 new terms) -- resin/ (local clone of Resinat/Resin, gitignored) - -## Total Goal Prompt - -Follow all grill decisions above. Set goal, start large build, complete -all T4 phases per the backlog. Always follow AGENTS.md, use ctx_* plugins, -use 1mcp exa/perplexity for web research, no hallucination. Ponytail full mode. -Use ultragoal to set goal for tracking. Research existing mature templates -instead of building from scratch. All features must have closed-loop tests. \ No newline at end of file diff --git a/docs/history/handoffs/HANDOFF_T5_TRACE_IPC.md b/docs/history/handoffs/HANDOFF_T5_TRACE_IPC.md deleted file mode 100644 index dab06aec..00000000 --- a/docs/history/handoffs/HANDOFF_T5_TRACE_IPC.md +++ /dev/null @@ -1,124 +0,0 @@ -# HANDOFF T5 — Trace Skeleton + IpcError Contract + Bug Fixes (2026-08-10) - -> All 10 grill questions answered. ADR-0026 ACCEPTED. -> CONTEXT.md updated with 3 new terms: Trace ID, IpcError, Account Strategy Tag. -> Domain audit: docs/GRILL_T5_DOMAIN_AUDIT.md. -> Decisions: docs/GRILL_T5_TRACE_IPC_BACKLOG.md. -> ADR: docs/adr/0026-accountid-bclass-strategy-ipc-trace.md. - -## Total Plan Table - -| Phase | Items | Scope | Est. Lines | Tests | Dependencies | -|-------|-------|-------|-----------|-------|---------------| -| 5-1 | Q1-Q5 trace skeleton | invokeWithTrace TS + trace.rs Rust + ResinClient #[instrument] | ~70 Rust + ~20 TS | 6 cargo + 3 vitest | None | -| 5-2 | Q6-Q8 IpcError contract | IpcError enum + map_resin_error + 44 command refactor + TS extractIpcErr | ~150 Rust + ~50 TS | 8 cargo + 6 vitest | Phase 5-1 | -| 5-3 | Bug 1-4 fixes | build-all.ps1 exact match + b_class default + bind conflict + protocol-aware prober | ~60 | 4 vitest | Phase 5-2 | -| 5-4 | Q10 B-class scheme c | account_for_bclass + strategy_engine B-class wiring + GUI B-class selector | ~100 Rust + ~30 TS | 4 cargo + 3 vitest | Phase 5-2 + 5-3 | -| 5-5 | i18n + build + push | 18-locale keys + build-all.ps1 + smoke + push | ~0 | i18n:check | All prior | - -## Phase 5-1: Trace Skeleton (Q1-Q5) - -### Frontend (src/lib/ipc.ts) -- new `invokeWithTrace(cmd: string, args?: Record<string, unknown>): Promise<unknown>` - - generates `const trace_id = crypto.randomUUID()` - - injects `__trace_id: trace_id` into args - - calls `invoke(cmd, args)` -- console.log `[trace_id]` for frontend-side correlation -- each existing wrapper (ipcPlatformAdd, ipcPlatformRemove, ...) replaced to call invokeWithTrace - -### Backend (src-tauri/src/trace.rs) -- `pub fn extract_trace_id(args: &serde_json::Value) -> Option<String>` - - reads `args["__trace_id"]`, validates UUID v4 format, returns Some(id) or None -- `pub fn trace_ipc_span(cmd: &str, trace_id: &str) -> tracing::Span` - - returns `tracing::info_span!("ipc.<cmd>", trace_id = %trace_id)` -- each #[tauri::command] entry: `let _span = trace_ipc_span("platform_add", &extract_trace_id(&args)?.unwrap_or("none")).entered();` -- ResinClient methods: add `#[instrument(skip(self), fields(trace_id))]` (tracing auto-propagates) - -### Tests -- cargo: extract_trace_id_valid_uuid, extract_trace_id_rejects_garbage, extract_trace_id_none_when_missing, trace_ipc_span_carries_id (4) -- cargo: instrument_span_propagates_to_subspan, instrument_span_on_error_logs_trace_id (2) -- vitest: invokeWithTrace_always_carries_trace_id, invokeWithTrace_generates_uuid_v4, invokeWithTrace_injects_into_args (3) - -## Phase 5-2: IpcError Contract (Q6-Q8) - -### Rust (crates/resin-core/src/ipc_error.rs) -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "kind", content = "data")] -pub enum IpcError { - BindConflict { port: u16, i18n_key: String }, - InvalidStrategy { value: String, accepted: Vec<String>, i18n_key: String }, - ResinUpstream { status: u16, excerpt: String, i18n_key: String }, - Internal { msg: String, i18n_key: String }, -} -``` -- `pub fn map_resin_error(status: u16, body: &str) -> IpcError` -- each #[tauri::command] return type: `Result<T, IpcError>` -- `impl From<serde_json::Error> for IpcError` (Internal) -- `impl From<reqwest::Error> for IpcError` (ResinUpstream) - -### Frontend (src/lib/ipc.ts) -```typescript -type IpcError = - | { kind: "BindConflict"; data: { port: number; i18n_key: string } } - | { kind: "InvalidStrategy"; data: { value: string; accepted: string[]; i18n_key: string } } - | { kind: "ResinUpstream"; data: { status: number; excerpt: string; i18n_key: string } } - | { kind: "Internal"; data: { msg: string; i18n_key: string } }; - -function extractIpcErr(e: unknown): IpcError { ... } -``` -- GUI catches IPC errors, calls extractIpcErr, routes to i18n key - -### Tests -- cargo: IpcError serde round-trip 4 variants (4) -- cargo: map_resin_error bind_conflict, map_resin_error invalid_strategy, map_resin_error upstream_5xx, map_resin_error unknown_fallback, map_resin_error 403_forbidden, map_resin_error 400_validation (6) -- vitest: extractIpcErrBindConflict, extractIpcErrInvalidStrategy, extractIpcErrResinUpstream, extractIpcErrInternal, extractIpcErrI18nKeyPresent, extractIpcErrTemplateParams (6) - -## Phase 5-3: Bug Fixes (Bug 1-4) - -- Bug 1: scripts/build-all.ps1 Step 5 `$bundles` filter: `-match "EgressAPIKEY"` -> `Name -eq "egressapikey-headless.exe"` + exclude deps/ -- Bug 2: src/views/PlatformsView.tsx L86 default `b_class: "balanced"` -> `b_class: "random"`; L456 same -- Bug 3: covered by IpcError::BindConflict (Phase 5-2) -- Bug 4: src-tauri/src/commands/mod.rs port_health_check: add protocol param, branch on socks5/http/none; GUI PortAuthInfo: conditional SOCKS5 creds display based on protocol - -## Phase 5-4: B-Class Strategy Scheme C (Q10) - -### Rust (crates/resin-core/src/strategy_engine.rs) -- new `pub fn account_for_bclass(strategy: StrategyId, port_label: &str) -> String` - - Random -> `port_label::random` - - Sequential -> `port_label::rr-1` (shell rotates account index) - - Latency -> `port_label::latency` - - Quality -> `port_label::quality` - - Bandwidth -> `port_label::bandwidth` - - ProtocolWeight -> `port_label::proto` -- new `pub fn account_for_fixed(port_label: &str) -> String` -> `port_label::fixed` -- round-robin: shell maintains an `AtomicUsize` counter per port, appends `::rr-{N}` where N rotates 0..max_nodes - -### GUI (src/views/PlatformsView.tsx) -- B-class selector sends strategy tag to port creation -- port creation embeds account string into Entry Port Mapping -- display: show account strategy tag in port card - -### Tests -- cargo: account_for_bclass_random, account_for_bclass_round_robin_rotation, account_for_bclass_latency_maps_to_prefer_low_latency, account_for_fixed_single_lease (4) -- vitest: bclass_selector_sends_tag, port_card_displays_strategy_tag, bclass_selector_changes_reflect_in_port_mapping (3) - -## Phase 5-5: i18n + Build + Push - -- Add i18n keys to all 18 locales: - - error.bindConflict (with {{port}}) - - error.invalidStrategy (with {{value}} {{accepted}}) - - error.resinUpstream (with {{status}}) - - error.internal -- pnpm i18n:check -- pnpm build (tsc + vite) -- cargo build --release -p egressapikey-app --features custom-protocol -- scripts/build-all.ps1 -- Smoke: MainWindowTitle + resin.exe child + release exe -- codegraph sync . -- git add + commit + push - -## Total Test Budget: 23 new tests -- cargo: 18 (6 trace + 8 IpcError + 4 B-class) -- vitest: 12 (3 trace + 6 IpcError + 3 B-class) -- 23 tests matches Q8b estimate \ No newline at end of file diff --git a/docs/history/handoffs/HANDOFF_T6_NETWORK_LAYER.md b/docs/history/handoffs/HANDOFF_T6_NETWORK_LAYER.md deleted file mode 100644 index 4548561f..00000000 --- a/docs/history/handoffs/HANDOFF_T6_NETWORK_LAYER.md +++ /dev/null @@ -1,51 +0,0 @@ -# T6 Handoff: Network Layer Build - -Date: 2026-08-12 -Status: COMPLETE — all nine T6 phases verified complete 2026-08-12 - -## Startup prompt for next window - -始终遵循 AGENTS.md,加载并使用 ctx_*插件,必要时用 1mcp 的 exa/perplexity 联网搜索,不要产生幻觉推理。Ponytail full 模式。 -Ponytail full mode. Ultragoal for goal persistence. -Research templates/wheels via pwm pro gpt56, not self-developed. -Sequence + acceptance + test closed-loop per capability. -Every platform (cargo + vitest) needs test closed-loop. - -## Execution plan - -See docs/GRILL_T6_NETWORK_LAYER_PLAN.md for full T6-1 through T6-9 detail. - -### Sequencing + acceptance criteria - -| Step | Item | Acceptance criteria | Test closed-loop | -|------|------|-------------------|-----------------| -| T6-1 | WhiteboxConfig.network + DNS struct | cargo test green: network validation | cargo | -| T6-2 | sidecar.rs env injection | cargo test green: 7 env vars conditional | cargo | -| T6-3 | GUI Settings network card | vitest green: card + save + reset + i18n | vitest + i18n | -| T6-4 | E2E test tool | cargo mockito + vitest diagnostics button | cargo + vitest | -| T6-5 | Firewall + 0-nodes + request log | cargo request_log_tail + vitest banner | cargo + vitest | -| T6-6 | IpcError wiring + restart banner | cargo IpcError + vitest restart banner | cargo + vitest | -| T6-7 | Diagnostics GUI panel | vitest panel renders | vitest | -| T6-8 | All tests pass | cargo + vitest + i18n + tsc green | all | -| T6-9 | Build + smoke + push | exe + chunk hash + smoke + codegraph + push | smoke | - -### Dependencies -T6-1 -> T6-2 -> T6-3 -> (T6-4 || T6-5) -> T6-6 -> T6-7 -> T6-8 -> T6-9 - -## Completion summary (2026-08-12) - -All nine T6 phases executed against the standing plan and verified by code-level audit: - -| Phase | Deliverable | Tests | -|---|---|---| -| T6-1 | `WhiteboxConfig.network` + `validate_network` + 5 cargo | green | -| T6-2 | `sidecar.rs` 7 `RESIN_*` env injections + `read_network_config` + 3 cargo | green | -| T6-3 | GUI Settings network card + `whitebox_save_network` IPC + 3 vitest | green | -| T6-4 | `probe_exit_ip` IPC + socks feature (fix `6acccd5`) + `proxy_e2e.rs` + `parse_trace_body_ip` extraction + #[ignore] 1.1.1.1 | 3 cargo + 3 vitest | -| T6-5 | `check_firewall_status` + `request_log_tail` IPC + 0-nodes amber banner | 3 vitest | -| T6-6 | 4-state sidecar banner (healthy/unhealthy/restarting/terminated) + port rebind sync | 2 vitest | -| T6-7 | Diagnostics panel: sidecar PID + healthz last check + IPC latency + port + mode + firewall + request log | 3 vitest | -| T6-8 | 122 cargo + 3 proxy_e2e (1 ignored) + 168 vitest = 293 total, tsc green, 274 i18n keys / 18 locales | all green | -| T6-9 | `cargo build` green + codegraph sync + all commits pushed to `codex/rust-port` HEAD `13483a2` | green | - -ADR-0028 created post-audit to record the `WhiteboxConfig.network` decision per the plan's own ADR item. `docs/PONYTAIL_DEBT_LEDGER.md` re-verified: three live `ponytail:` source tags (1 keep, 1 done, 1 stale-tag); stale ledger entries for `route_id` / `normalize_auth` / notification-crate refilled since the underlying code is gone. See the ledger for the re-audit receipt. diff --git a/docs/history/handoffs/HANDOFF_T8_EXIT_IP_STRATEGY.md b/docs/history/handoffs/HANDOFF_T8_EXIT_IP_STRATEGY.md deleted file mode 100644 index 254ab2d7..00000000 --- a/docs/history/handoffs/HANDOFF_T8_EXIT_IP_STRATEGY.md +++ /dev/null @@ -1,62 +0,0 @@ -# T8 Exit IP Strategy Fix Branch - Handoff - -> Branch: `codex/rust-port` | HEAD: `5988c7d` (pushed) | Date: 2026-08-14 - -## Completed Phases - -| Phase | Status | Summary | -|-------|--------|---------| -| T8-1 | Done | Circuit breaker: passive_circuit_breaker_disabled in platform PATCH + system_config_get/patch IPC + circuitBreakerThreshold system config | -| T8-2 | Done | Strategy verify: strategy_verify IPC probes N requests (3-50) via Resin proxy, collects exit IP + latency distribution, DiagnosticsView panel with platform dropdown, sample count, result visualization | -| T8-3 | Done | Connection control: close_all_connections + reset_kernel IPC (kill sidecar child via Mutex Option Child + sidecar_restart helper), DiagnosticsView panel with confirm dialogs | -| T8-4 | Done | Sticky TTL: default changed from 168h0m0s to 0s (disable domain stickiness so random/round-robin strategies work correctly) | -| T8-5 | Done | Subscription update_interval: subscription_add accepts update_interval Option String param (default 30s, validated against Resin) | -| T8-6 | Done | Close/Reset IPC: both commands kill sidecar, reset additionally restarts; SidecarHandle.child is Mutex Option Child for safe kill | -| T8-7 | Done | Final gate: tsc green, vitest 182 passed, cargo 118 passed, i18n 304 keys, diff --check clean | - -## Key Decisions (Q1-Q5) - -| Q | Decision | Rationale | -|---|----------|-----------| -| Q1 | A - Expose Resin passive_circuit_breaker_disabled + max_consecutive_failures | Shell exposes existing Resin mechanism, no fork needed | -| Q2 | A+C - strategy_verify IPC + DiagnosticsView panel | Verify strategies work + surface in diagnostics center | -| Q3 | A - sticky_ttl default to 0s | User wants no domain stickiness; sticky breaks random exit IP | -| Q4 | A - subscription_add accepts update_interval | Whitebox configurable sync interval | -| Q5 | C - Both close_all_connections + reset_kernel via kill+restart | Resin has no graceful connection close API; kill sidecar drops all TCP | - -## Files Changed (22 files, +526 lines, -1 line) - -### Rust backend -- crates/resin-core/src/resin_client.rs - system_config_get/patch methods added -- src-tauri/src/commands/mod.rs - system_config_get/patch, strategy_verify, close_all_connections, reset_kernel IPC + passive_circuit_breaker_disabled in platform_update + sticky_ttl default 0s + update_interval in subscription_add -- src-tauri/src/main.rs - invoke_handler registrations for all new commands - -### TS frontend -- src/lib/ipc.ts - ipcSystemConfigGet/Patch, ipcStrategyVerify, ipcCloseAllConnections, ipcResetKernel wrappers + circuitBreakerDisabled param in ipcPlatformUpdate + updateInterval in ipcSubscriptionAdd -- src/views/DiagnosticsView.tsx - Strategy verify panel (platform dropdown, sample count, result bar chart) + Connection control panel (close all + reset kernel buttons with confirm) - -### i18n -- All 18 locales src/locales/*/common.json - 10 new keys (304 total): strategy.circuitBreaker, strategy.circuitBreakerThreshold, strategy.stickyTtlHint, subscription.updateInterval, diagnostics.closeAllConnections, diagnostics.resetKernel, strategyVerify.*, connectionControl.* - -## Test Results - -- vitest: 182 passed / 14 files / 0 failures -- cargo: 118 passed / 0 failed / 0 ignored -- tsc: zero errors -- i18n: 304 keys x 18 locales, all match en canonical -- git diff --check: CLEAN (no CRLF) - -## Build - -- npx vite build green - chunks CzuPrGlZ (index) + CxDMLfHj (TopologyView) -- cargo build --release -p egressapikey-app --features custom-protocol - 12.94MB exe -- Chunk hash verified inside exe bytes (AGENTS section 5 hard close-loop) -- Staged at release/windows-gui/EgressAPIKEY.exe + resin.exe sidecar (36.17MB) - -## Architecture Notes - -- Circuit breaker: Resin v1.2.0 exposes passive_circuit_breaker_disabled (boolean, per-platform via PATCH) and max_consecutive_failures (integer, system-wide via GET/PATCH). Shell exposes both through IPC; no fork needed. -- Strategy verify: shell calls http://127.0.0.1:api_port/proxy_token/https/api.ipify.org with X-Resin-Account header, N times (3-50), collects unique exit IPs + latencies. -- Sticky TTL: Resin default 168h0m0s (7 days) causes domain stickiness. Changed to 0s at the IPC layer so Resin receives 0s by default; user can still override via platform_update if needed (whitebox). -- Close/reset: Resin has no graceful drop all connections API. close_all_connections kills the sidecar child process (drops all active TCP). reset_kernel kills + restarts via sidecar_restart(). -- SidecarHandle.child changed to Mutex Option Child so the kill path can .take() the child once and call .kill() safely; a second kill finds None and no-ops. \ No newline at end of file diff --git a/docs/history/handoffs/HANDOFF_T9_CROSS_PLATFORM_BUILD.md b/docs/history/handoffs/HANDOFF_T9_CROSS_PLATFORM_BUILD.md deleted file mode 100644 index 6a0b6fa8..00000000 --- a/docs/history/handoffs/HANDOFF_T9_CROSS_PLATFORM_BUILD.md +++ /dev/null @@ -1,48 +0,0 @@ -# HANDOFF_T9_CROSS_PLATFORM_BUILD.md - -> **Commit**: aad9858 on codex/rust-port (pushed) -> **Date**: 2026-08-14 -> **Token usage**: 327,868 / 100,000,000 - -## What was done - -Grill T9 (Q1-Q11) planned + executed in one session: - -1. **fetch_resin.sh** — SCRIPT_DIR + REPO_ROOT anchoring (T9-1): cwd-independent path resolution -2. **build-all.sh** — 6 changes (T9-2 to T9-7): - - SCRIPT_DIR anchoring + MINGW64 explicit (T9-2) - - Removed lowercase egressapikey from find search, unified to EgressAPIKEY (T9-3) - - Test step: cargo test -p resin-core --lib + vitest run + i18n:check (T9-4) - - Backend tar.gz staging: resin-core + egressapikey-headless (T9-5) - - Three-platform flow guard: Windows direct, macOS open, Linux DISPLAY shield (T9-6) - - SHA256 checksum generation for all staged artifacts (T9-7) -3. **verify-build.sh** — CI/local split (T9-8): CI=true runs cargo test --workspace, local runs cargo test -p resin-core --lib -4. **ci.yml** — portable job matrix.bin: egressapikey -> EgressAPIKEY (T9-9) - -## Verification - -| Check | Result | -|-------|--------| -| bash syntax (3 scripts) | SYNTAX OK | -| git diff --check | clean (no CRLF) | -| cargo test -p resin-core --lib | 118 passed | -| vitest | 182 passed (14 files) | -| i18n:check | 304 keys / 18 locales OK | -| tsc + vite build | green | -| git push | aad9858 pushed to codex/rust-port | - -## Files changed - -- scripts/fetch_resin.sh (+6/-2) -- scripts/build-all.sh (+83/-35) -- scripts/verify-build.sh (+39/-35) -- .github/workflows/ci.yml (+4/-4) -- docs/GRILL_T9_CROSS_PLATFORM_BUILD.md (new, plan table) -- docs/adr/0030-cross-platform-build-compat.md (new, ADR) - -## Total goal plan prompt - -Always follow AGENTS.md, load and use ctx_* plugins, use 1mcp exa/perplexity for web research when needed, no hallucination. Ponytail full mode. -Set goals with ultragoal to prevent task loss. Research existing templates/wheels rather than self-developing. -Break down priority order, acceptance criteria, and test closed-loops to avoid introducing features without completing them. -Use pwm pro search research for every technical detail to avoid hallucination, and every platform must have test closed-loops. diff --git a/docs/history/phases/GRILL_ISSUES_BACKLOG.md b/docs/history/phases/GRILL_ISSUES_BACKLOG.md deleted file mode 100644 index a0767329..00000000 --- a/docs/history/phases/GRILL_ISSUES_BACKLOG.md +++ /dev/null @@ -1,164 +0,0 @@ -# GRILL ISSUES BACKLOG (Route-corrected — 2026-08-05) - -> ADR-0012 (route correction) + ADR-0013 (rename to EgressAPIKEY) + ADR-0014 -> (dead code deletion) are ACCEPTED. This backlog reflects the post-correction -> state. Items marked SUPERSEDED are kept for audit trail only. - -## Q1-Q11 Decision Summary (this grill round) - -| Q | Topic | Decision | ADR | -|---|---|---|---| -| Q1 | Reuse Resin vs build new | Reuse Resin (Ponytail) | ADR-0012 | -| Q2 | Architecture: thin shell A vs fork C | Path A (thin shell, not fork) | ADR-0012 | -| Q3 | HTTP vs SOCKS5 identity | HTTP for unified port; multi-port for per-key isolation | ADR-0012 | -| Q4 | GUI + whitebox config sync | A+B both (GUI for masses, whitebox for power users, atomic sync) | ADR-0012 | -| Q5 | Config hot-reload mechanism | hotswap-config (community-vetted wheel) | ADR-0012 | -| Q6 | Commit granularity | Small step commits (Ponytail) | — | -| Q7 | Strategy layer + AI stream sensor + IP reputation | Modular pluggable strategies (not global); independent AI stream sensor board; IP reputation via external API wheels (IPQualityScore/AbuseIPDB/ip-api) | ADR-0012 | -| Q8 | IP reputation scoring | Use open-source mature wheels, not self-built; IPQualityScore (5K/mo free) + AbuseIPDB (1K/day) + ip-api.com (45/min) | ADR-0012 | -| Q9 | Project rename | EgressAPIKEY (no collision, audited 5 sources) | ADR-0013 | -| Q10 | Dead code: delete vs keep vs refactor | (a) Delete all dead code (Ponytail clean break) | ADR-0014 | -| Q11 | Backlog audit:失效/保留/重定义 | See tables below | — | - -## 失效项 (SUPERSEDED — kept for audit trail only) - -| Item | Reason | -|---|---| -| C1-1 (observed_keys SQLite + route_id GUI chips) | route_id based on reading Authorization header; HTTPS makes this impossible. SUPERSEDED by ADR-0012. | -| ADR-0003 (key+endpoint identification via header) | Same premise error. SUPERSEDED by ADR-0012. | -| ADR-0011 (B-B-3 route_id-derived key identity) | Same. SUPERSEDED by ADR-0012. SQLite infra reused; observed_keys schema deleted. | -| P24-A4-3 (axum interceptor) | Entire file deleted per ADR-0014. | -| C2-3 (keyCandidates cross-view persistence) | keyCandidates concept (manual endpoint+apiKey input) abolished. Replaced by Entry Ports. | -| C2-4 (auto vs manual platform card visual) | key candidate concept gone; platform card visual stays but binds to ports not keys. | -| TopologyView B-column route_id chip rendering | Deleted. B column now renders port-based identity. | - -## 保留项 (unaffected by route correction) - -| Item | Status | Notes | -|---|---|---| -| C1-2 (hot-connect atomic PATCH + state refresh) | confirmed | Topology edge PATCH semantics unchanged | -| C1-3 (drag-delete edge = remove region_filters) | confirmed | Idempotent removal unchanged | -| C1-4 (viewport memory + initial fitView) | confirmed | Pure frontend, identity-agnostic | -| C2-1 (subscription drag reorder persists) | done=607460a | Unaffected | -| C2-2 (subscription rename/delete coexistence) | done=192bc82 | Unaffected | -| C2-5 (tray i18n real-time sync) | done=55f6583 | Unaffected | -| C2-7 (Settings locale switch re-renders canvas) | done=d109e86 | Unaffected | -| C2-8 (Settings dirty-state sticky save bar) | done=f359c82 | Unaffected | -| C2-9 (log system maturation) | done (P14 `f359c82`/P18 `main.rs` L24 panic hook + L70-72 tauri-plugin-tracing Rotation::Daily + MaxFileSize::mb(10) + KeepSome(7)) | Implemented in prior phase; verified code-level | -| C2-10 (path protection / privilege escalation) | done (P14 `commands/mod.rs` L964-965 canonicalize+starts_with backups confinement, backup zip_path traversal HIGH fixed) | Implemented in prior phase | -| C2-11 (env var / OS side-effect protection) | done (G3 `sidecar.rs` spawn_health_poll: clears OS proxy on 3 consecutive /healthz fail, restores on recovery; no env var injection from webview) | Implemented in prior phase | -| C2-12 (second instance guard + focus) | done (`main.rs` L43 tauri_plugin_single_instance::init — focuses + unminimizes existing window on second launch) | Implemented in prior phase | -| C2-13 (close GUI keeps tray icon) | done (`main.rs` L78-85 on_window close requested handler hides window instead of exiting; tray Quit is the real exit path) | Implemented in prior phase | -| P25burst-1 (reset-sort duplicates) | done (P20 fix) | Unaffected | -| P25burst-2 (node pool stats vs table mismatch) | done (P20 fix) | Unaffected | -| P25burst-3 (import 0 nodes) | done (P13 clash UA fix) | Unaffected | -| P25burst-4 (platform add OK but not shown) | done (C2-14) | Unaffected | -| Subscriptions CRUD + clash UA fetch | live | Unaffected | -| Node pool / IP channels | live | Unaffected | -| Ghost safety net | live | Unaffected | -| Sidecar lifecycle | live | Unaffected | -| Backup/config export-import | live | Unaffected | - -## 重定义项 (concept survives, binding changes) - -| Item | Old | New | -|---|---|---| -| TopologyView A column | Single conceptual entry node | Multiple actual entry ports (socks5/http), each port = one identity | -| TopologyView B column | Platforms with route_id chips | Platforms with port-based identity chips (port number + protocol) | -| PlatformsView left pane | keyCandidates (endpoint+apiKey) | Entry Ports (port number + protocol + status) | -| PlatformsView right pane | Platforms (drag key -> platform) | Platforms (drag port -> platform) | -| ProcessRouteView | Process -> lane mapping | Process -> port mapping (lane concept deprecated) | -| gateway_reserve/release/evict_lane IPC | Lane-based echo commands | Port-based or deleted (Resin owns semantics) | -| REFACTOR_PLAN.md Phase R2-R4 | R2 canvas + R3 node mgmt + R4 config | R2 canvas (A col = ports) + R3 node mgmt (unchanged) + R4 config (add port->platform mapping) | -| C2-14 (platform create dialog) | Create platform + drag key candidate | Create platform + drag entry port to bind | - -## 新增项 (did not exist before route correction) - -| Item | Description | Priority | -|---|---|---| -| NEW-1 | Multi-port socks5/http listener core (tokio TcpListener + protocol detection) | **done (P2)** — `port_forwarder.rs` + reload tests | -| NEW-2 | Port -> (platform, account) mapping table (SQLite, reuses DbPool) | **done (P2)** — `db.rs` user_version=2 `port_mappings` | -| NEW-3 | Port-identity injection on forward (Resin V1 Platform.Account via SOCKS5/HTTP auth) | **done (P2)** — `resin_identity` + proxy auth rewrite | -| NEW-4 | Modular strategy layer (pluggable: liveness, latency, bandwidth, quality, protocol weight) | high — Q7 decision | -| NEW-5 | AI stream sensor module (SSE/WS awareness, independent board) | high — Q7 decision | -| NEW-6 | IP reputation integration (IPQualityScore + AbuseIPDB + ip-api, pluggable) | medium — Q8 decision | -| NEW-7 | hotswap-config (whitebox config layer, atomic backup, hot-reload) | **done (P3)** — validated file watch + transactional SQLite/listener/file reload | - -## P3 progress (2026-08-05) - -- **NEW-7 complete**: hotswap-config-backed whitebox entry-port document, transactional DB/listener/file swap, hand-edit reload IPC and Settings surface. -- **NEW-4 complete (Resin-compatible scope)**: strategy catalog maps supported user choices to Resin's three live allocation policies; it does not fabricate a per-node selector. -- **NEW-5 complete (header-visible scope)**: independent stream sensor counts HTTP unary/SSE/WebSocket classifications without TLS termination, body inspection, or key interception. -- **P2 closed-loop strengthened**: two actual HTTP entry ports forward to a loopback Resin mock, inject distinct port-derived identities, and replace attacker-supplied proxy authorization. -- **Validation**: cargo test -p resin-core --lib: 82 passed; pnpm test: 103 passed; pnpm exec tsc --noEmit; pnpm i18n:check: 18 locales / 171 keys; staged release/windows-gui/EgressAPIKEY.exe embeds index-C-jBubhx, smoke title EgressAPIKEY with Resin child. - -| NEW-8 | Project rename to EgressAPIKEY (repo, Cargo.toml, tauri.conf, package.json, README, AGENTS) | high — Q9 decision | -| NEW-9 | Dead code deletion (interceptor.rs, route_id, observed_keys, ADR-0003/0011 superseded) | blocking — Q10 decision | -| NEW-10 | MEMORY_REUSE_DECISION.md update (route correction conclusion固化) | medium — documentation | -| NEW-11 | New HANDOFF document (post-correction execution plan) | medium — handoff | - -## Execution order (dependency graph) - -NEW-9 (delete dead code) -> NEW-8 (rename) -> NEW-1 (multi-port listener) -> -NEW-2 (port->platform mapping) -> NEW-3 (X-Resin-Account injection) -> -NEW-7 (hotswap-config) -> NEW-4 (strategy layer) -> NEW-5 (AI stream sensor) -> -NEW-6 (IP reputation) -> 重定义项 (TopologyView/PlatformsView rewrite) -> -保留 pending 项 (C2-9/10/11/12/13) - -## Open Questions (pending grill — none at this time) - -All Q1-Q11 resolved. Next grill round starts when execution hits a new -ambiguity or the user raises a new concern. - -## Grill v1.2.0 Audit (Q12 — Resin v1.2.0 alignment) - -| Q | Topic | Decision | ADR | -|---|---|---|---| -| Q12 | Resin v1.2.0 endpoint API vs port_forwarder redundancy | T1 (Thin-Shell Downgrade): port_forwarder deletes protocol handling, keeps StreamSensor; 5 IPC forward to endpoint API; fetch_resin REL → v1.2.0; shell DB stays as port→platform metadata | ADR-0015 | - -### T1 Execution Plan (dependency graph) - -| Step | Task | Status | -|------|------|--------| -| T1-1 | ADR-0015 + backlog update | done | -| T1-2 | fetch_resin.{ps1,sh} REL v1.1.2→v1.2.0 + fetch binary | done (0b913a0) | -| T1-3 | ResinClient: 5 endpoint methods + mockito tests | done (0b913a0, 88 cargo pass) | -| T1-4 | port_forwarder.rs: delete protocol handling, keep StreamSensor + identity + constants | done (0b913a0, 791→208 lines) | -| T1-5 | db.rs PortMapping field align with endpoint schema | done (schema already aligned; port primary key) | -| T1-6 | commands/mod.rs port_* IPC → endpoint API forwarder + atomic werbox layering | done (this commit; +#[tauri::command]) | -| T1-7 | main.rs PortForwarder::new wiring → ResinClient + StreamSensor | done (0b913a0, removed 2 fwd.reload fallbacks) | -| T1-8 | frontend ipc.ts PortMapping type alignment | done (ipcPortList/Upsert/Remove/Running/Reload exist since P2) | -| T1-9 | Build: pnpm build + cargo build --release + stage + smoke + codegraph sync | done (this commit, chunk CCDf4dc8 embedded, 13.25 MB, smoke green) | -| T1-10 | Phase 6 remaining: C2-9/10/11/12/13 + C1-2/3/4 polish | done (AGENTS §31 verified) | - -## T2 Execution Plan (Grill Q1-Q7 round 2026-08-06) - -| Step | Task | ADR | Status | -|------|------|-----|--------| -| T2-1 | sidecar.rs: RunningMode enum + ArcSwap<State> + refactor SidecarHandle | 0016 | done (sidecar.rs RunningMode enum, ArcSwap replaced with AtomicU8 state, commit 0e21ec2) | -| T2-2 | sidecar.rs: stderr CommandEvent -> RingBuffer (500 lines) + IPC get_sidecar_logs + Tauri event push | 0016 | done (VecDeque ring buffer + IPC get_sidecar_logs, commit 0e21ec2) | -| T2-3 | sidecar.rs: Crash auto-restart (3x bounded, 1s/2s/4s backoff) + try_wait death short-circuit | 0016 | done (crash_backoff_ms + MAX_CRASH_RESTARTS=3, 1s/2s/4s pattern; ponytail: dead_code marker for wiring, commit 0e21ec2) | -| T2-4 | sidecar.rs: Port cleanup before spawn (detect stale process on free port) | 0016 | done (check_port_available pure fn + port_hint in boot timeout, commit 63e89e5) | -| T2-5 | sidecar.rs: Two-phase shutdown (SIGTERM -> 500ms -> try_wait -> SIGKILL -> reap) | 0016 | done (two_phase_shutdown_result + SHUTDOWN_WAIT_MS + PID reap, commit 0a32185) | -| T2-6 | docs/RESIN_UPSTREAM_MANIFEST.yaml: create manifest + fetch_resin.{ps1,sh} read from it | 0017 | done (RESIN_UPSTREAM_MANIFEST.yaml v1.2.0, commit 9e34c55) | -| T2-7 | AGENTS.md: update 11 stale v1.1.2 refs to manifest version | 0017 | done (AGENTS.md v1.1.2->v1.2.0 refs updated, commit 9e34c55) | -| T2-8 | resin_client.rs: read-only method auto-retry (2x, 500ms) + mockito 503->200 test | 0018 | done (send_read retry 2x/500ms + 3 mockito tests, commit cec337c, 91 cargo pass) | -| T2-9 | scripts/build-all.ps1: PowerShell build+stage+SHA256+smoke pipeline | 0019 | done (build-all.ps1 PowerShell pipeline + SHA256, commit 6a645f8) | -| T2-10 | Closed-loop tests per capability (ADR-0020): ring buffer, crash restart mock, two-phase shutdown mock, manifest parse, retry mockito, ps1 stage SHA256 | 0020 | done (ring buffer test, crash restart mock, two-phase shutdown test, manifest parse test, retry mockito, ps1 stage - all green, 91 cargo + 106 pnpm) | -| T2-11 | Release exe rebuild + stage + smoke + codegraph sync | 0005 | done (release/windows-gui/EgressAPIKEY.exe staged, smoke verified) | - -Dependency: T2-1 -> (T2-2 || T2-3 || T2-4 || T2-5) -> T2-6 -> T2-7 || T2-8 -> T2-9 -> T2-10 -> T2-11 - -### Resin log volume fact (measured 2026-08-06) - -Measured by running resin v1.2.0 binary locally: -- stdout: 0 lines (all output goes to stderr) -- stderr boot spam: 29 lines / 1.75 KB (all in <1s at boot) -- stderr after boot with 25 API requests: 0 new lines -- Conclusion: Resin is quiet after boot. 500-line ring buffer covers days of operation. - -### Resin upstream repo (固化防止遗忘) - -https://github.com/Resinat/Resin -Current pinned version: v1.2.0 -License: MIT diff --git a/docs/history/phases/GRILL_T10_PLATFORM_GUI_REFACTOR.md b/docs/history/phases/GRILL_T10_PLATFORM_GUI_REFACTOR.md deleted file mode 100644 index f13495b8..00000000 --- a/docs/history/phases/GRILL_T10_PLATFORM_GUI_REFACTOR.md +++ /dev/null @@ -1,92 +0,0 @@ -# GRILL T10 — Platform GUI Refactor (2026-08-14) - -> Branch: codex/rust-port (parallel fix branch) -> Date: 2026-08-14 -> Status: PLANNED (all Q1-Q6 decisions confirmed) -> Budget: 100,000,000 tokens -> Skills: ponytail:full, grill-with-docs, domain-modeling -> Supersedes: ADR-0029 T8 partial implementation (chip selectors never landed) - -## Q&A Decision Summary - -| Q | Topic | Decision | ADR | -|---|---|---|---| -| Q1 | A/B strategy layout on platform card | A — Left/right split card: A-class and B-class each occupy 50% width of lower half, both use ToggleGroup chip | 0031 | -| Q2 | A-class chip-based multi-select | A — Full chip implementation: manual=nodeList checkbox chips, region=distinct regions toggle chips, subscription=subscriptionList toggle chips, quality=top_n input + live Top-3 preview | 0031 | -| Q3 | Port auth toggle persistence | A — Persist to settings.json via loadPortAuthDefault/savePortAuthDefault, same level as splitRatio | 0031 | -| Q4 | Global scrollbar styling | A — Pure CSS scrollbar-width:thin + ::-webkit-scrollbar (6px, semi-transparent, overlay), in src/styles.css, zero deps | 0031 | -| Q5 | Remove B-class text summary from card top | A — Delete L434 summary line, move leases/nodes counts to right-top pill, B-class name only appears in lower chip UI | 0031 | -| Q6 | Smart port number suggestion | A — New IPC port_suggest (from 17990 increment + skip used + TcpListener::bind probe), frontend auto-fills on mount | 0031 | - -## Root cause analysis (code-level) - -### Current state (PlatformsView.tsx 539 lines) - -| Area | Current Code | Problem | -|------|-------------|---------| -| A/B layout | B-class `<select>` right-top, A-class panel below `mt-2.5 border-t pt-2` | Not side-by-side, visual asymmetry | -| A-class manual | `<option value="manual">` only, no input UI | Zero selection capability — selecting manual shows nothing | -| A-class region | Text input + "+" button | User must hand-type region codes | -| A-class subscription | Text input + "+" button | User must hand-type subscription names | -| A-class quality | `top_n` number input only | No live preview of which nodes are Top-3 | -| Port auth toggle | `useState(true)`, no persistence | Forgets user's choice every page mount | -| Scrollbar | No CSS in styles.css | Platform default 17px scrollbars, ugly | -| B-class summary | `t(strategyToI18nKey(p.allocationPolicy)) + " · " + leases + nodes` in L434 text | Redundant with lower chip UI after Q1=A split | -| Port number default | `useState("17990")` hardcoded | Never changes, no system-level occupancy check | - -### IPC availability (already exist, not yet imported in PlatformsView) - -| IPC | Location | Returns | -|-----|----------|---------| -| `ipcNodeList()` | `src/lib/ipc.ts:141` | `Promise<unknown>` (raw JSON: node_hash, display_tag, region, tags) | -| `ipcSubscriptionList()` | `src/lib/ipc.ts:237` | `SubscriptionSnapshotEntry[]` (name, node_count) | -| `loadPortAuthDefault/savePortAuthDefault` | NOT YET — new in settings.ts | boolean persist via tauri-plugin-store | -| `port_suggest` | NOT YET — new IPC in commands/mod.rs | u16 first available port | - -## T10 Execution Plan - -| Step | Task | Files | ADR | Test | -|------|------|-------|-----|------| -| T10-1 | A/B left-right split card layout: platform card lower half divided 50/50, left=A-class ToggleGroup chip, right=B-class ToggleGroup single-select chip, both styled identically. Delete old `<select>` B-class and old A-class text panel. Remove L434 summary text line, add leases/nodes pill to card right-top. | src/views/PlatformsView.tsx | 0031 | 2 vitest (renders split layout, B-class chip changes allocationPolicy) | -| T10-2 | A-class chip-based multi-select: (a) manual → import ipcNodeList, render each node as checkbox chip (blue=selected, click=remove), selected stored in strategyConfig.platforms[].manual_nodes (b) region → from ipcNodeList extract distinct regions, toggle chips (click highlight/cancel) (c) subscription → from ipcSubscriptionList render toggle chips (d) quality → top_n input + live preview "Top-3: HK(100ms), JP(150ms), US(200ms)" sorted from nodeList | src/views/PlatformsView.tsx, src/lib/ipc.ts (add NodeEntry TS interface) | 0031 | 4 vitest (one per a_class type chip render + selection round trip) | -| T10-3 | Port auth toggle persistence: new loadPortAuthDefault/savePortAuthDefault in settings.ts, newAuthRequired initialized from load, saved on toggle change, useEffect on mount loads, onChange saves | src/lib/settings.ts, src/views/PlatformsView.tsx | 0031 | 1 vitest (persist + restore) | -| T10-4 | Global thin scrollbar CSS: 6 lines in src/styles.css — `scrollbar-width:thin` + `scrollbar-color` + `::-webkit-scrollbar{width:6px}` + thumb + dark variant | src/styles.css | 0031 | 0 (CSS-only, visual) | -| T10-5 | Remove B-class text summary from card top: delete L434 line, move leases.length + routableNodeCount to right-top pill badge alongside delete button | src/views/PlatformsView.tsx | 0031 | 1 vitest (no B-class name in summary, counts in pill) | -| T10-6 | Smart port suggestion: new #[tauri::command] port_suggest(db, used_ports) in mod.rs — from 17990 increment + skip port_mappings used + TcpListener::bind probe, return first available. New ipcPortSuggest in ipc.ts. Frontend useEffect on mount calls ipcPortSuggest and fills newPort. | src-tauri/src/commands/mod.rs, src/lib/ipc.ts, src/views/PlatformsView.tsx | 0031 | 1 cargo (port_suggest skips used + returns free) + 1 vitest (auto-fills on mount) | -| T10-7 | i18n: new keys for strategy chips across all 18 locales. Keys: strategy.aClassTitle, strategy.bClassTitle, strategy.manualSelect, strategy.regionSelect, strategy.subscriptionSelect, strategy.qualityPreview, platform.leasesPill, platform.nodesPill, platform.portSuggested | src/locales/*/common.json (18 files) | 0031 | i18n:check green | -| T10-8 | Build + smoke: pnpm build + cargo build --release, stage exe, grep chunk hash, smoke launch | scripts/build-all.sh | 0031 | hard close-loop per AGENTS §5 | - -## Execution dependencies - -``` -T10-4 (CSS scrollbar) — parallel, no dependencies -T10-3 (auth persist) — parallel, no dependencies -T10-6 (port_suggest) — parallel (Rust IPC + TS wrapper) -T10-1 (split card layout) — must complete before T10-2 -T10-2 (chip multi-select) — depends on T10-1 layout + ipcNodeList -T10-5 (remove summary) — depends on T10-1 layout -T10-7 (i18n) — after all UI steps (T10-1, T10-2, T10-3, T10-5, T10-6) -T10-8 (build+smoke) — last, after all steps -``` - -## Research references - -- shadcn ToggleGroup: https://ui.shadcn.com/docs/components/toggle-group — chip-based multi-select industry standard -- Radix ToggleGroup: https://www.radix-ui.com/primitives/docs/components/toggle-group — supports single/multiple selection, keyboard nav -- shadcn Badge: https://ui.shadcn.com/docs/components/badge — removable chip variant -- MDN scrollbar-width: https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-width — Baseline 2024, `thin` keyword -- clash-verge-rev ProxyGroup pattern: chip-based region/node selection (not text input) -- env-manager scrollbar: thin overlay (visual reference) - -## Verification criteria (per step) - -| Step | Verify | -|------|--------| -| T10-1 | tsc green + vitest renders A/B side-by-side + B chip click fires ipcPlatformUpdate | -| T10-2 | tsc green + 4 vitest (manual chip from nodeList, region toggle, subscription toggle, quality preview) | -| T10-3 | tsc green + vitest auth toggle persists across remount | -| T10-4 | tsc green (CSS-only, no test needed) | -| T10-5 | tsc green + vitest no B-class name in summary text | -| T10-6 | cargo test green + tsc green + vitest newPort auto-filled on mount | -| T10-7 | i18n:check = previous key count + 9, all 18 locales match | -| T10-8 | pnpm build green + cargo build --release green + chunk hash grep in exe + smoke launchMainWindowTitle=EgressAPIKEY | diff --git a/docs/history/phases/GRILL_T11_PLATFORM_PAGE_UX_POLISH.md b/docs/history/phases/GRILL_T11_PLATFORM_PAGE_UX_POLISH.md deleted file mode 100644 index 9fa6c7b7..00000000 --- a/docs/history/phases/GRILL_T11_PLATFORM_PAGE_UX_POLISH.md +++ /dev/null @@ -1,87 +0,0 @@ -# GRILL T11 — Platform Page UX Polish - -> Supersedes T10 platform GUI refactor for the platform page surface. -> ADR-0032 records the architectural decisions. -> All file edits via ctx_* per AGENTS context-mode routing. - ---- - -## Q1-Q8 Decision Summary - -| Q | Topic | Decision | Key Change | -|---|-------|----------|-------------| -| Q1 | Platform card expand/collapse | A — Collapsible | collapsed=summary row (name + A/B badges + leases/nodes pill + chevron); expanded=full A/B chip selectors. shadcn Collapsible pattern. | -| Q2 | Strategy selected visual feedback | B — Ring feedback | Extra ring-2 ring-primary ring-offset-1 on selected chips, in addition to Q1 badge. | -| Q3 | Manual node search + prefix filter | A+hybrid | Inline search filters chip list; selected nodes stay visible at top in "Selected (N)" grouped section even when filtered out. | -| Q4 | Subscription "partial not found" + stale platforms | C — Per-platform sync | Delete global Apply button; each chip change directly PATCHes the specific platform. Fix Manual bug in strategy_engine.rs where a_class_regions returns strategy.regions instead of mapping manual_nodes to regions. Auto-clean stale platform entries from strategyConfig. | -| Q5 | Port form flash on mount | A — Bootstrap gate | Skeleton until persisted settings loaded, then render form. No default value flash. | -| Q6 | Port card click toggle select | A — Toggle select | selectedPort: number|null state, click toggles, selected=ring-2 ring-primary, enables keyboard Delete. | -| Q7 | Port form compact layout | A — Single inline row | port(w-20) + protocol(w-24) + label(flex-1) + auth checkbox + add button inline. Delete grid-cols-2. | -| Q8 | Port card compact collapsing | A — Collapsible | collapsed=single row (port/protocol + health dot + auth icon + delete); expanded=details. Matches Q1 pattern. | - ---- - -## Root Cause Analysis - -### Bug 4 (subscription "partial not found") - -**Location**: `src-tauri/src/commands/mod.rs:2586-2615` `strategy_apply` - -**Root cause**: The command iterates `strategyConfig.platforms` and calls `platform_id_for_name()` for each. If the config contains a `platform_name` that was deleted from Resin (stale entry), `platform_id_for_name` returns `None` and the result shows `patched: false, reason: "platform not found"`. - -**Secondary bug**: `crates/resin-core/src/strategy_engine.rs:165` `a_class_regions` for `AClassStrategy::Manual` returns `strategy.regions.clone()`. But the GUI Manual mode writes node hashes to `manual_nodes`, NOT region codes to `regions`. So `compute_plan` never uses `manual_nodes` and Manual strategy produces empty or stale regions. - -**Fix**: -1. Fix `a_class_regions` Manual case to map `manual_nodes` (node hashes) to regions via the healthy nodes list. -2. Auto-clean stale entries: before iteration, filter `strategyConfig.platforms` to only those that exist in the live Resin platform list. -3. Switch to per-platform sync mode (Q4-C): each chip change PATCHes the individual platform directly, no global Apply button. - -### Bug 5 (port form flash on mount) - -**Location**: `src/views/PlatformsView.tsx:50-54` - -**Root cause**: `useState("17990")`, `useState("Default")`, `useState(true)` initialize with hardcoded defaults. The `useEffect` that loads persisted values runs AFTER the first paint, so users see the defaults flash before the real values load. - -**Fix**: Add a `bootstrapped` gate state. While `false`, render a skeleton placeholder. After the first `useEffect` loads persisted values and sets `bootstrapped=true`, render the real form. Same pattern as P15 App.tsx bootstrap gate. - ---- - -## Execution Plan (10 Steps) - -| Step | Task | Files | Test | Deps | -|------|------|-------|------|------| -| T11-4a | Fix a_class_regions Manual bug | strategy_engine.rs | 2 cargo | none | -| T11-4b | Per-platform sync + auto-clean stale | PlatformsView.tsx, mod.rs | 3 vitest + 2 cargo | T11-4a | -| T11-1 | Platform card collapsible | PlatformsView.tsx | 2 vitest | T11-4b | -| T11-2 | Chip ring feedback | PlatformsView.tsx | 1 vitest | T11-1 | -| T11-3 | Manual node search + hybrid | PlatformsView.tsx | 2 vitest | T11-1 | -| T11-5 | Port form bootstrap gate | PlatformsView.tsx | 1 vitest | none | -| ~~T11-6~~ | ~~Port card toggle select~~ (REMOVED in T13: blue ring selection deleted) | PlatformsView.tsx | N/A | N/A | -| T11-7 | Port form compact inline | PlatformsView.tsx | 1 vitest | T11-5 | -| T11-8 | Port card collapsible | PlatformsView.tsx | 2 vitest | (T11-6 removed) | -| T11-9 | i18n 18 locales | src/locales/*/common.json | i18n:check | all UI | -| T11-10 | Build + smoke + chunk hash | build-all.ps1 | hard close-loop | all | - -Dependencies: T11-4a first (root cause fix). T11-4b after 4a. T11-1 after 4b. T11-2/T11-3 after 1. T11-5/6/7/8 can parallel with 1-3. T11-9 after all UI. T11-10 last. - ---- - -## Acceptance Criteria - -- [ ] `cargo test -p resin-core --lib` passes with new Manual mapping tests -- [ ] `pnpm test` passes with all new vitest closed-loop assertions -- [ ] No global Apply button in PlatformsView -- [ ] Platform cards collapsible with A/B badges in collapsed state -- [ ] Selected chips show ring-2 ring-primary ring-offset-1 -- [ ] Manual node search filters chips + selected nodes stay visible at top -- [ ] No port form default value flash on mount -- [ ] Port card click toggles selection (ring + keyboard Delete) -- [ ] Port form is a single inline row -- [ ] Port cards collapsible (collapsed=single row) -- [ ] `pnpm i18n:check` green (all 18 locales match) -- [ ] Release exe embeds latest Vite chunk hash (grep verified) -- [ ] Smoke launch: MainWindowTitle=EgressAPIKEY, no panic - ---- - -## Status: PLANNING COMPLETE, AWAITING EXECUTION diff --git a/docs/history/phases/GRILL_T12_PLATFORM_PAGE_FIX.md b/docs/history/phases/GRILL_T12_PLATFORM_PAGE_FIX.md deleted file mode 100644 index 8508b5ba..00000000 --- a/docs/history/phases/GRILL_T12_PLATFORM_PAGE_FIX.md +++ /dev/null @@ -1,111 +0,0 @@ -# GRILL T12 — Platform Page UX Fix (Fix Branch) - -**Date**: 2026-08-14 -**Status**: PLANNING COMPLETE, AWAITING EXECUTION -**Prerequisite**: T11 complete (ADR-0032), PlatformsView 660 lines - -## Q1-Q5 Decision Summary - -| Q | Topic | Decision | Key Change | -|---|-------|----------|-------------| -| Q1 | Port selected gray opacity | C — No fix | Drag opacity-50 is transient; selected ring-primary is persistent. No real conflict. | -| Q2 | Disable WebView2 right-click context menu | C — CSS + config double guard | App root oncontextmenu=preventDefault + global CSS user-select:none on body + tauri.conf.json if available. | -| Q3 | Port card hover auto-expand | A — hoveredPort state | onMouseEnter sets hoveredPort, onMouseLeave clears. Expand = expandedPortCards.has OR hoveredPort. Manual expand persists; hover expand is transient. | -| Q4 | Manual node Selected(N) refactor | OK — Reuse NodesView pattern | Add tags field to NodeEntry. Reuse subName + groupBySub from NodesView. Manual mode shows nodes folded by subscription source. Delete Selected(N) section. Selected nodes highlighted blue in-place within their subscription group. | -| Q5 | Port form label input compact | C — min-w-0 max-w-200px | Change label input from flex-1 to min-w-0 max-w-[200px] flex-1. Ensures auth checkbox + add button always visible when middle pane is narrow. | - -## Execution Plan (5 Steps) - -| Step | Task | Files | Test | Deps | -|------|------|-------|------|------| -| T12-1 | Disable WebView2 context menu (CSS + config) | App.tsx, index.css, tauri.conf.json | 1 vitest | none | -| T12-2 | Port card hover auto-expand | PlatformsView.tsx | 2 vitest | none | -| T12-3 | Manual mode: subscription-folded node list | PlatformsView.tsx (NodeEntry tags) | 2 vitest | none | -| T12-4 | Port form label input compact | PlatformsView.tsx | 1 vitest | none | -| T12-5 | Build + smoke + chunk hash | build-all.ps1 | hard close-loop | all | - -Dependencies: T12-1/2/3/4 can all parallel. T12-5 last. - -## Detailed Specs - -### T12-1: Disable WebView2 context menu -- App.tsx: root <div> gets onContextMenu={(e) => e.preventDefault()} -- src/index.css or tailwind: body { -webkit-user-select: none; user-select: none; } but exempt input/textarea -- tauri.conf.json: check if Tauri 2 has windows[].disableContextMenu; if not, CSS+JS is sufficient -- 1 vitest: verify App root div has onContextMenu handler - -### T12-2: Port card hover auto-expand -- New state: const [hoveredPort, setHoveredPort] = useState<number | null>(null) -- onMouseEnter={() => setHoveredPort(p.port)} on <li> -- onMouseLeave={() => setHoveredPort(null)} on <li> -- Expand condition: const isExpanded = expandedPortCards.has(p.port) || hoveredPort === p.port -- Manual expand (chevron click) adds to expandedPortCards (persistent); hover is transient -- 2 vitest: (1) hover expands port card, (2) manual expand persists after mouse leave - -### T12-3: Manual mode subscription-folded node list -- PlatformView NodeEntry interface: add tags?: { subscription_name?: string; subscriptionName?: string; tag: string }[] -- nodeList mapping (L218): add tags: n.tags ?? [] -- Inline subName + groupBySub logic from NodesView (or import) -- Manual mode replaces flat Selected(N) + flat unselected list with: - - Group nodes by subName(n) || "untagged" - - Each group: collapsible header (subscription name + count + chevron) - - Default collapsed, click to expand - - Within expanded group: node chips with blue bg if selected, normal if not - - Search input still filters across all groups -- 2 vitest: (1) manual mode shows subscription group headers, (2) selected nodes highlighted in their group - -### T12-4: Port form label compact -- Change L395 from: <input className="flex-1 rounded border ..." - to: <input className="min-w-0 max-w-[200px] flex-1 rounded border ..." -- 1 vitest: verify label input has max-w-[200px] class - -### T12-5: Build + smoke + chunk hash -- pnpm build -> note chunk hash -- cargo build --release -p egressapikey-app --features custom-protocol -- Stage exe to release/windows-gui/EgressAPIKEY.exe -- Grep chunk hash in exe bytes -- Smoke: launch, verify MainWindowTitle=EgressAPIKEY, WS 30-40MB - -## Root Cause Analysis - -### Bug 1 (port selected gray) -Not a bug. opacity-50 is only during drag (pointerdown to pointerup). selected ring-primary is after click. No real overlap. No fix needed (Q1=C). - -### Bug 2 (WebView2 context menu) -No oncontextmenu handler anywhere in the app. WebView2 shows native right-click menu. Fix: global preventDefault + CSS user-select:none (Q2=C). - -### Bug 3 (no hover expand) -expandedPortCards only toggled by chevron click. No hover behavior. Fix: add hoveredPort transient state (Q3=A). - -### Bug 4 (Manual Selected(N) ugly) -PlatformsView NodeEntry missing tags field. NodesView already has groupBySub pattern. Fix: add tags, reuse grouping, delete Selected section (Q4=OK). - -### Bug 5 (label input too wide) -L395 flex-1 with no max-w. Narrow middle pane squeezes out auth checkbox + add button. Fix: min-w-0 max-w-[200px] flex-1 (Q5=C). - -## Acceptance Criteria - -1. Right-click anywhere in app triggers no WebView2 context menu -2. Hover port card auto-expands showing auth details; mouse away auto-collapses; chevron click persists expand -3. Manual mode shows nodes grouped by subscription source, each group collapsible, selected nodes blue-highlighted, no Selected(N) section -4. Port form label input never wider than 200px; auth checkbox and + button always visible when middle pane narrow -5. pnpm test all pass; npx tsc --noEmit clean; cargo test -p resin-core --lib pass; i18n:check pass -6. Release exe embeds fresh Vite chunk hash; smoke green - -## T12-fix (post-T12): port card selection removal + stale closure fix - -### Bug 1: Port card blue ring selection removed -- selectedPort state, togglePortSelect function, isSelected, ring-2 ring-primary all deleted -- Port cards no longer have click-to-select behavior; drag threshold (5px) controls opacity-50 -- Keyboard Delete handler removed (was depending on selectedPort) -- Tests updated: T11-6 test removed, T12-fix click test now asserts NO ring-2 - -### Bug 2: Strategy chip selection not persisting (stale closure) -- Root cause: syncPlatformStrategy read strategyConfig from stale closure, not the latest state -- Fix: updateAndSync now captures latestConfig inside setStrategyConfig callback and passes it to syncPlatformStrategy -- syncPlatformStrategy now accepts optional configToSync parameter -- updateStrategyField removed (was the culprit for stale state) -- Tests: T12-persist validates strategy_config_put is called after chip selection - -### Bug 3: Top-N quality strategy not persisting -- Same root cause as Bug 2 (stale closure). Fixed by the same updateAndSync rewrite. diff --git a/docs/history/phases/GRILL_T13_CANVAS_V2_PLAN.md b/docs/history/phases/GRILL_T13_CANVAS_V2_PLAN.md deleted file mode 100644 index bd1e7c68..00000000 --- a/docs/history/phases/GRILL_T13_CANVAS_V2_PLAN.md +++ /dev/null @@ -1,114 +0,0 @@ -# GRILL T13 CANVAS V2 PLAN - -> Fix branch: TopologyView canvas — strategy-driven dagre layout + flash fix + zustand cache -> Predecessors: T9 (canvas fix commit c45334c), T10 (platform GUI), T11 (platform UX), T12 (platform drag fix) -> ADR-0034 - -## Grill Decisions (Q1-Q4) - -| Q | Topic | Decision | Industry Template | -|---|-------|----------|-------------------| -| Q1 | C 列节点摆布模式 | A — 策略驱动自动摆布 + Kiali Service Graph 风格 | Kiali Service Graph (Istio), ReactFlow data-driven edges | -| Q2 | 画布布局算法 | 甲 — dagre + reactflow 官方推荐 | @xyflow/react dagre example, dagre D3 hierarchical layout | -| Q3 | 空态闪现修复 | A — gate 前移,三条消息包入 opacity gate div | standard opacity gate (existing T9 onInit pattern) | -| Q4 | 缓存机制 | A — zustand 局部 store + shallow compare,零新依赖 | zustand/shallow (already installed), env-manager Svelte pattern | - -## Bug-to-Fix Mapping - -| User Bug | Root Cause | Fix Phase | -|----------|-----------|-----------| -| 1. C 列节点区域分类平铺,没按 B 策略自动展示 | buildEdges 只按 region_filters 匹配;C 列全量显示不过滤 | T13-1 + T13-2 | -| 2. 画布卡顿 + 连线锚点莫名其妙 | 手工硬编码 position (x:0/300/640); Handle full-area 互相挡 | T13-2 (dagre) + T13-3 (Handle fix) | -| 3. 左上角闪现空态文字 | 三条空状态消息在 opacity-0 gate 外面 | T13-4 | -| 4. 缺缓存机制,频繁 sync 卡顿 | 每次进入画布 sync() 4 IPC 并行 + 全量 setState | T13-5 | - -## Execution Plan (6 Phases) - -### Phase T13-1: C 列策略驱动过滤 (Q1=A) - -- parseSubscriptionGroups 改为两层过滤: - - L1 = subscription group (折叠 chip: sub name + 被选中节点数 / 总节点数) - - L2 = 只显示被至少一个平台 region_filters 命中 OR 未被选中但策略为 manual 的节点 - - 未被任何平台选中的节点折叠为 "+N 未绑定" 行 (不默认展开) -- 新增 helper: getSelectedRegions(platforms) — 遍历所有平台 region_filters 返回选中 region 集合 -- SubscriptionGroupNode 过滤: node rows 只渲染 selectedRegions 包含其 region 的节点 -- buildEdges 只画到被选中的 sub group, 未选中的不生成 B->C edge -- i18n: 1 new key topology.unbound x18 locales -- Test: vitest verifying getSelectedRegions + filtered nodeRows + unbound count -- ~120 lines refactor - -### Phase T13-2: dagre 自动布局 (Q2=甲) - -- Install dagre: pnpm add dagre @types/dagre (~60KB, mature) -- New helper layoutNodes(nodes, edges): 用 dagre.graphlib 构建有向图 - - rankdir=LR (left-to-right 三列), ranksep=80, nodesep=40 - - dagre 自动分配 {x,y}, 注入 node.position -- 删除手工 position: { x: 0/300/640, y: 60+i*130/120 } 硬编码 -- fitView 保留: onInit 里 dagre 布局完成后 fitView -- Test: vitest verifying layoutNodes output has valid positions -- ~80 lines new (layout helper) + ~20 lines position change -- Ponytail: dagre 是 @xyflow/react 官方自动布局推荐库 - -### Phase T13-3: Handle 锚点逻辑修复 (Q2 衍生) - -- Handle 定位修复: source/target Handle 改为固定右/左中点半圆, 不再 100% full-area 覆盖 - - T9-4 的 full-area Handle 是 "碰到盒子就连" 的正确意图, 但 opacity:0 + 100% 覆盖导致拖 card 时 Handle 吃掉 mousedown - - 改为: Handle 固定在 card 右/左边缘中点 radius 20px, 可见但小巧; connectionRadius=40 保持旁路 snap -- Test: vitest verifying Handle style has fixed position (not full-area) -- ~30 lines CSS + Handle prop changes - -### Phase T13-4: 空态闪现修复 (Q3=A) - -- Gate 前移: 三条空状态消息移入 opacity-0/100 gate div 内 -- sync() 完成后 setReady(true): 当前 ready 只在 onInit 触发, 改为 sync resolve 后也 setReady -- Test: vitest verifying empty-state messages only render inside opacity gate -- ~15 lines (CSS class + div nesting) - -### Phase T13-5: zustand 局部 store + shallow 缓存 (Q4=A) - -- New file src/store/topologyStore.ts: create<TopologyState> with slices: platforms, subGroups, leases, ports -- sync() 改写: fetch 4 IPC -> topologyStore.setState() with shallow skip -- TopologyView 改用 selector: useTopologyStore(s => s.platforms, shallow) 等 -- 恢复 5s 轮询: shallow compare 跳过相同值 setState, 不卡 -- visibilitychange 恢复: 窗口重新可见时立即 sync() -- Test: vitest verifying sync() with same data does not trigger re-render (shallow skip) -- ~60 lines new store + ~40 lines TopologyView refactor - -### Phase T13-6: 全量门禁 + exe stage + push - -- pnpm test (all vitest green) -- cargo test -p resin-core --lib (all cargo green) -- pnpm exec tsc --noEmit -- pnpm i18n:check (new keys x18 locales) -- pnpm build (Vite new chunk hash) -- cargo build --release -p egressapikey-app --features custom-protocol -- exe staged + chunk hash grep verify (AGENTS section 5 hard close-loop) -- codegraph sync . -- git push - -## Acceptance Criteria - -1. C 列只显示被至少一个平台选中的节点, 未选中的折叠为 "+N 未绑定" — visual + vitest -2. 画布布局由 dagre 自动计算, 无硬编码 position — vitest verifying layoutNodes -3. Handle 不再吃掉 card 拖拽 — vitest verifying Handle style -4. 进入画布不再闪现空态文字 — visual confirm -5. 5s 轮询恢复不卡 — vitest verifying shallow skip re-render -6. 182+ vitest + 120 cargo + tsc + i18n green - -## Dependencies - -- New dep: dagre + @types/dagre (npm, ~60KB, mature) -- Existing: @xyflow/react ^12.11.0, zustand ^5.0.0 (with shallow module already shipped) - -## Industry References (pplx + exa research) - -- Kiali Service Graph: Istio service mesh topology, strategy-driven edges, auto-hide untargeted nodes -- @xyflow/react dagre example: https://reactflow.dev/learn/layouting/layouting — official recommended auto-layout -- zustand/shallow: https://docs.pmnd.rs/zustand/guides/prevent-rerenders-with-use-shallow — official perf pattern -- env-manager ProfilePage.svelte: shallow compare + local fetch reuse (Svelte pattern reference) - -## Master Goal Prompt - -始终遵循 AGENTS.md,加载并使用 ctx_*插件,必要时用 1mcp 的 exa/perplexity 联网搜索,不要产生幻觉推理。Ponytail full 模式。 -以下功能都我希望你能联网调研出一个类似功能的模板轮子出来,而不是自己开发 -细分好先后顺序和验收标准和test闭环,避免只引入但是却没有做到的情况。 diff --git a/docs/history/phases/GRILL_T14_CANVAS_BUGFIX_PLAN.md b/docs/history/phases/GRILL_T14_CANVAS_BUGFIX_PLAN.md deleted file mode 100644 index a0123896..00000000 --- a/docs/history/phases/GRILL_T14_CANVAS_BUGFIX_PLAN.md +++ /dev/null @@ -1,85 +0,0 @@ -# GRILL T14 — Canvas Bug Fix + Node Compression Plan - -> grill-with-docs 流程规整结果。Q1-Q5 用户决策已固化。 - -## Bug 审计(代码级确认) - -### Bug 1: A/B 策略顺序翻转 -- **根因**: TopologyView.tsx 第 212-224 行,PlatformNode 组件先渲染 bClassLabel 再渲染 aClassLabel -- **修复**: 翻转渲染顺序,aClassLabel 在上,bClassLabel 在下 -- **验收**: vitest 验证 A 策略 badge 在 B 策略 badge 之前渲染 - -### Bug 2: A 策略 i18n 未同步 -- **根因**: 第 494 行 aClassLabel 硬编码 "region:" + region_filters.join(",").toUpperCase() 或 "manual",未走 t() -- **修复**: manual → t("strategy.manual")(已有 key);region → t("topology.aClassRegion", { regions }) 新增 key -- **新增 i18n**: topology.aClassRegion,en: "Region: {{regions}}",× 18 locales -- **验收**: vitest 验证 aClassLabel 走 t() + i18n:check green - -### Bug 3: C 列冗余节点卡片 -- **根因**: 第 509-550 行对每个 subGroup 逐个渲染 filteredNodes 节点行 -- **修复**: C 列混合视图(Q1=C),默认按订阅折叠(Q5=A),可切换按地区视图(Q4=A) -- **折叠状态**: 订阅名 + 健康率 + 地区列表(如 "JP(53) US(12) SG(8)") -- **展开**: 点击展开前 10 个节点行,超出显示 "+N more" -- **切换控件**: CanvasControls 顶部 segmented toggle [订阅] | [地区] -- **验收**: vitest 验证折叠/展开 + 切换 + 节点数 > 10 时只渲染 10 行 - -### 新增: "回到世界中心"按钮 -- **行为 (Q2=A)**: 视口重置到 {x:0, y:0, zoom:1}(坐标系原点 + 100% 缩放) -- **位置**: CanvasControls 按钮组,新增第 5 个按钮(Home 图标) -- **i18n**: 新增 topology.resetCenter × 18 locales -- **验收**: vitest 验证 setViewport({x:0,y:0,zoom:1}) 被调用 - -## 执行计划(6 Phases) - -### Phase T14-1: Bug 1 — A/B 策略顺序翻转 -- 翻转 PlatformNode 渲染顺序:aClassLabel 在上,bClassLabel 在下 -- Test: vitest 验证 A badge 在 B badge 之前 -- ~5 行改动 - -### Phase T14-2: Bug 2 — A 策略 i18n 同步 -- 第 494 行替换硬编码为 t() 调用 -- 新增 i18n key topology.aClassRegion 带 {{regions}} 插值 × 18 locales -- manual 复用 t("strategy.manual") -- Test: vitest 验证 aClassLabel 走 t() + i18n:check -- ~10 行 + 18 locale 文件 - -### Phase T14-3: Bug 3 — C 列按订阅折叠(默认视图) -- 改写 SubscriptionGroupNode:折叠状态聚合信息,展开前 10 个节点 + "+N more" -- useState 管理展开/折叠 -- 改写 rawNodes 构建:聚合地区统计而非逐个渲染 -- Test: vitest 验证折叠/展开 + 节点数 > 10 时只渲染 10 行 -- ~80 行重构 - -### Phase T14-4: C 列按地区视图(切换视图) -- 新增 viewMode state: "subscription" | "region" -- region 视图:按地区分组,每地区卡片显示节点总数 + 健康率 -- 边连到地区而非订阅 -- Test: vitest 验证切换 viewMode 后 C 列按地区渲染 -- ~60 行新增 - -### Phase T14-5: 视图切换控件 + "回到世界中心"按钮 -- CanvasControls 顶部新增 segmented toggle [订阅] | [地区] -- CanvasControls 新增第 5 个按钮(Home 图标),点击 setViewport({x:0,y:0,zoom:1}) -- 新增 i18n: topology.resetCenter + topology.viewSubscription + topology.viewRegion × 18 locales -- Test: vitest 验证切换 toggle + 回到中心按钮 -- ~40 行新增 - -### Phase T14-6: 全量门禁 + exe stage + push -- pnpm test / tsc --noEmit / i18n:check / pnpm build / cargo build --release / exe stage / chunk hash / codegraph sync / git push - -## 验收标准 - -1. PlatformNode 上 A 策略 badge 在 B 策略 badge 之上 — vitest -2. A 策略标签走 t(),"manual" 和 "region:XX" 都有 i18n — vitest + i18n:check -3. C 列默认按订阅折叠,展开显示前 10 个节点 — vitest -4. 可切换为按地区视图 — vitest -5. CanvasControls 顶部有 [订阅] | [地区] segmented toggle — vitest -6. CanvasControls 新增"回到世界中心"按钮,重置视口到 {x:0,y:0,zoom:1} — vitest -7. 191+ vitest + tsc + i18n green - -## 联网调研结论 - -- dagre 支持 compound graph(subgraph/cluster 布局) -- ReactFlow 节点超 100 个明显卡顿,折叠为聚合卡片是行业共识 -- Kiali Service Graph 默认聚合视图,点击展开子节点 -- 心智模型: 画布 = 路由流向可视化(入口→策略→出口),不是节点全览 diff --git a/docs/history/phases/GRILL_T14_PERFORMANCE.md b/docs/history/phases/GRILL_T14_PERFORMANCE.md deleted file mode 100644 index 13016e75..00000000 --- a/docs/history/phases/GRILL_T14_PERFORMANCE.md +++ /dev/null @@ -1,143 +0,0 @@ -<!-- T14 COMPLETION STATUS: all 9 phases done, committed, pushed --> - -# GRILL T14 — Performance Optimization Plan - -> **Status**: All Q1-Q8 decisions user-approved. Plan ready for execution. -> **Target**: Reduce memory from 171MB → 50-80MB, prevent resin.exe orphan leak, industrial-grade performance with closed-loop tests. - -## Decisions (Q1-Q8) - -| # | Area | Decision | Template Reference | -|---|------|----------|-------------------| -| Q1 | Sidecar orphan prevention | (A) Windows Job Object | clash-verge-rev PR #6853 | -| Q2 | WebView2 memory reduction | (A) Full optimization | clash-verge-rev + Taskbar Sentinel | -| Q3 | IPC cache strategy | (C) Introduce SWR library (4.2KB) | Vercel SWR | -| Q4 | Lightweight mode | (A) clash-verge-rev full state machine + EmptyWorkingSet | clash-verge-rev lightweight.rs + cockpit-tools PR #686 | -| Q5 | Poll pause | (A) usePoll unified hook | Yerd usePoll pattern | -| Q6 | zustand shallow selector | (A) Full migration across all 6 Views | React 19 + zustand/shallow | -| Q7 | SWR integration | (A) Unified useIpc hook layer | SWR + Tauri IPC bridge | -| Q8 | Additional perf areas | (A) Both: emit/Channel guard + NodesView virtualization | Tauri issue #13758 + @tanstack/react-virtual | - -## Execution Plan (9 Phases) - -### T14-1: Windows Job Object (sidecar.rs) -- Add `windows-sys` crate to `src-tauri/Cargo.toml` -- After `Command::spawn`, call `CreateJobObjectW` + `SetInformationJobObject` (JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) + `AssignProcessToJobObject` -- Store `OwnedHandle` for job in `SidecarHandle` (RAII closes handle → OS kills child) -- On `RunEvent::Exit`: existing `child.kill()` + `child.wait()` stays; Job Object is belt-and-suspenders for Task Manager force-kill -- Known limitation: spawn→assign race window (document, same as clash-verge-rev PR #6853) -- Test: 2 cargo tests (job_handle_drops_kills_child, assign_fails_returns_err) -- Files: `src-tauri/src/sidecar.rs`, `src-tauri/Cargo.toml` -- Validation: cargo test pass - -### T14-2: Lightweight mode (lightweight.rs) -- New file `src-tauri/src/lightweight.rs`: `LightweightState` enum (Normal/In/Exiting) + `try_transition` CAS -- `setup_window_close_listener`: on `tauri://close-requested` start delay timer (default 10min, configurable in Settings) -- `setup_webview_focus_listener`: on `tauri://focus` cancel delay timer -- `entry_lightweight_mode`: `window.destroy()` + tray update + `EmptyWorkingSet` (Windows) / `malloc_trim(0)` (Linux) / `malloc_zone_pressure_relief` (macOS) -- `exit_lightweight_mode`: `WebviewWindowBuilder::from_config` rebuild + restore listeners -- i18n: add `settings.lightweightMode` + `settings.autoLightweightMinutes` keys across 18 locales -- Settings: add toggle + minutes input in SettingsView -- Test: 6 cargo tests (state transitions, CAS, timer cancel) -- Files: `src-tauri/src/lightweight.rs`, `src-tauri/src/main.rs`, `src/views/SettingsView.tsx`, `src/store/appStore.ts`, locale files -- Validation: cargo test pass + pnpm i18n:check green - -### T14-3: usePoll unified hook (src/hooks/usePoll.ts) -- New `src/hooks/usePoll.ts`: `usePoll(intervalMs, asyncFn)` hook -- Auto-pause on `document.hidden` → `clearInterval` -- Auto-resume on visible → immediate `asyncFn()` + restart `interval` -- Unmount → `clearInterval` + `AbortController.abort()` -- In-flight dedup via `useRef<AbortController | null>` -- Migrate all 6 Views: replace their `setInterval/useEffect` polling with `usePoll` -- Test: 6 vitest (pause on hidden, resume on visible, abort on unmount, dedup in-flight, interval cleanup, immediate sync on focus) -- Files: `src/hooks/usePoll.ts`, all 6 View files -- Validation: vitest pass + tsc green - -### T14-4: zustand shallow selector migration -- `appStore.ts`: keep structure, add `export const useShallowAppStore = (selector) => useAppStore(selector, shallow)` -- All 6 Views: replace `const { x, y } = useAppStore()` with individual selectors -- Each View subscribes only to its own slice -- Test: 3 vitest (shallow skip same value, partial update triggers only relevant component, full store update still works) -- Files: `src/store/appStore.ts`, all 6 View files -- Validation: vitest pass + tsc green - -### T14-5: SWR library + useIpc hook (src/hooks/useIpc.ts) -- `pnpm add swr` -- New `src/hooks/useIpc.ts`: `useIpc(key, fetcher, opts?)` wrapping `useSWR` -- Stable IPC dedup key generation + default 10s TTL (`dedupingInterval: 10000`) -- AbortController integration + Tauri-external graceful fallback -- Migrate 6 Views: replace `useEffect + invoke` with `const { data, error, mutate } = useIpc("key", ipcXxx)` -- Background refresh: `usePoll(10s, () => mutate())` for live data -- Test: 6 vitest (cache hit, cache expiry, dedup, abort on unmount, Tauri-external fallback, mutate refresh) -- Files: `src/hooks/useIpc.ts`, `package.json`, all 6 View files -- Validation: vitest pass + tsc green - -### T14-6: emit/Channel guard + documentation -- Audit all `app_handle.emit()` calls: verify payload size < 1KB (small = OK) -- Document in AGENTS.md: "emit for small events (<1KB), ipc::Channel for streaming/large data" -- Add lint comment in `commands/mod.rs` at each emit site -- Test: 2 cargo tests (emit payload size check, channel streaming smoke) -- Files: `src-tauri/src/commands/mod.rs`, `src-tauri/src/sidecar.rs`, `AGENTS.md` -- Validation: cargo test pass - -### T14-7: NodesView virtualization (@tanstack/react-virtual) -- `pnpm add @tanstack/react-virtual` -- Rewrite NodesView table to use `useVirtualizer` for 295+ rows -- Only visible rows (~20-50) render in DOM -- Test: 3 vitest (virtual render only visible, scroll loads more, row click still works) -- Files: `src/views/NodesView.tsx`, `package.json` -- Validation: vitest pass + tsc green - -### T14-8: Build + test + smoke + i18n check -- `pnpm build` → note chunk hash -- `cargo build --release -p egressapikey-app --features custom-protocol` -- Stage exe to `release/windows-gui/` -- Grep chunk hash in exe bytes -- Smoke: MainWindowTitle + WorkingSet + resin child alive -- `pnpm i18n:check` all 18 locales -- `cargo test -p resin-core --lib` -- `pnpm test` -- Validation: ALL green - -### T14-9: Doc update + ADR + push -- New `docs/adr/0035-performance-optimization.md` -- Update `AGENTS.md` with new sections: lightweight mode, usePoll, useIpc, SWR cache -- Update `docs/GRILL_T14_PERFORMANCE.md` (this file) -- Commit + push -- Validation: git push success - -## Research References - -- clash-verge-rev PR #6853: Windows Job Object for sidecar orphan prevention -- clash-verge-rev lightweight.rs: full state machine + auto timer + listener management -- cockpit-tools PR #686: Destroy WebView + EmptyWorkingSet/malloc_trim/malloc_zone_pressure_relief -- cc-switch: AtomicBool LIGHTWEIGHT_MODE + window.destroy() -- Taskbar Sentinel: 12MB install / 35MB idle via webview on-demand loading -- PowerShift: UI separated from background agent (346MB UI vs 13MB background) -- Yerd usePoll: document.visibilityState pause + in-flight dedup -- wry 0.35: WebViewExtWindows::set_memory_usage_level API -- Tauri issue #13758: Eval/ExecJS memory leak, use ipc::Channel for large data -- LogRocket SWR vs TanStack Query: SWR 4.2KB vs TanStack 11.4KB -- Microsoft WebView2 perf docs: MemoryUsageTargetLevel.Low + TrySuspendAsync -- Tauri IPC bandwidth wall: large Vec<u8> over invoke = JSON array (use custom protocol instead) - -## New Dependencies -- `windows-sys` crate (T14-1) — Windows Job Object API -- `swr` npm package (T14-5) — ~4.2KB gzipped, Vercel SWR -- `@tanstack/react-virtual` npm package (T14-7) — ~2.5KB gzipped, list virtualization - - -## Completion summary - -All 9 T14 phases completed and verified: -- T14-1 Job Object ✅ -- T14-2 Lightweight mode ✅ -- T14-3 usePoll ✅ -- T14-4 useShallow ✅ -- T14-5 SWR/useIpc ✅ -- T14-6 emit/Channel guard audit ✅ -- T14-7 Virtualization ✅ -- T14-8 SettingsView UI + IPC ✅ -- T14-9 ADR-0035 + docs + push ✅ - -Verified: 209 vitest pass, 119 cargo pass, tsc green, i18n 323 keys × 18 locales, smoke exe ~39MB (77% reduction from 171MB), fresh bundle hash confirmed in exe bytes. diff --git a/docs/history/phases/GRILL_T15_CANVAS_FIX_PLAN.md b/docs/history/phases/GRILL_T15_CANVAS_FIX_PLAN.md deleted file mode 100644 index 130a2746..00000000 --- a/docs/history/phases/GRILL_T15_CANVAS_FIX_PLAN.md +++ /dev/null @@ -1,108 +0,0 @@ -# GRILL T15 — Canvas Bug Fix + Strategy Pipeline Unification Plan - -> grill-with-docs 流程规整结果。Q1-Q5b 用户决策已固化。 -> Q5 ADR: [ADR-0036](../docs/adr/0036-strategy-single-source-truth-config.md) - -## Bug 审计(代码级确认) - -### Bug 1: C 列冗余订阅卡片(Q1=A) -- **根因**: TopologyView.tsx L559 `subGroups.forEach` 无条件遍历所有订阅组,即使没有平台选中该订阅的任何 region,卡片仍然渲染 -- **修复**: 在 forEach 内增加过滤条件 —— 如果该订阅的所有节点 region 都不在 `selectedRegions` 中,跳过该订阅组不渲染 -- **验收**: vitest 验证无平台选中 region 时 C 列卡片不渲染 -- ~5 行改动 - -### Bug 2: CanvasControls 按钮宽度(Q2=B) -- **根因**: L436 所有按钮在 flex-col 容器里默认 stretch 同宽 -- **修复**: segmented toggle 保持全宽;icon 按钮(ZoomIn/ZoomOut/fitView/Home/Lock)加 `w-fit self-center` -- **验收**: vitest 验证 icon 按钮有 w-fit class -- ~5 行 className 改动 - -### Bug 3: viewMode/locked 状态不持久化(Q3=A) -- **根因**: L490 `useState("subscription")` / L434 `useState(false)` 纯内存,切换页面/重启后丢失 -- **修复**: - - settings.ts: `TopologyViewport` 接口扩展为 `TopologyState { x,y,zoom,viewMode,locked }` - - `loadTopologyViewport` → `loadTopologyState` 返回完整对象 - - `saveTopologyViewport` → `saveTopologyState` 接收完整对象 - - TopologyView: useState 初始值从 loadTopologyState 读取 - - onMoveEnd 回调保存完整状态(含 viewMode/locked) - - 保留 P20 opacity gate(ready state)防止闪现回归 -- **验收**: vitest 验证 loadTopologyState 返回 viewMode + locked + viewport -- ~40 行改动(settings.ts + TopologyView) - -### Bug 4: dagre 布局不从中心生成(Q4=A) -- **根因**: `layoutNodesViaDagre` 只做了中心点→左上角转换,没做图 bounding box 居中偏移 -- **修复**: dagre.layout 后读取 `g.graph()` 获取 {width,height},每节点 position 再减 (width/2, height/2),使图中心对齐到 (0,0) -- **验收**: vitest 验证布局后节点中心点在坐标系原点附近 -- ~5 行改动 - -### Bug 5: 画布策略绕过白盒配置文件管道(Q5=A, Q5b=A) -- **根因**: TopologyView onConnect/onEdgesDelete 直接 `ipcPlatformUpdate` PATCH Resin,绕过了 PlatformsView 的 strategyConfig JSON 管道 -- **修复**: - - 画布拖线 onConnect: 改为先读取 strategyConfig -> 更新对应平台的 regionFilters 字段 -> `ipcStrategyConfigPut` + `ipcStrategyApply` - - 画布删除连接 onEdgesDelete: 同上,从 regionFilters 移除对应 region - - 画布 sync() 时也从 strategyConfig 读取 region_filters(而非仅从 Resin platform 对象) - - 引入 `ipcStrategyConfigGet` / `ipcStrategyConfigPut` / `ipcStrategyApply` 到 TopologyView -- **验收**: vitest 验证画布拖线调用 strategyConfigPut 而非直接 platformUpdate -- ~60 行改动 - -## 执行计划(7 Phases) - -### Phase T15-1: Bug 1 — 隐藏完全未绑定的订阅卡片 -- subGroups.forEach 增加 selectedRegions 过滤 -- Test: vitest 验证无平台选中时 C 列无卡片 -- ~5 行 - -### Phase T15-2: Bug 2 — CanvasControls icon 按钮 w-fit -- icon 按钮加 `w-fit self-center` className -- Test: vitest 验证 icon 按钮有 w-fit -- ~5 行 - -### Phase T15-3: Bug 3 — topologyState 持久化 -- settings.ts: TopologyViewport → TopologyState { x,y,zoom,viewMode,locked } -- load/saveTopologyState helper -- TopologyView: useState 初始从 loadTopologyState 读取 -- onMoveEnd 保存完整状态 -- 保留 P20 opacity gate -- Test: vitest 验证 loadTopologyState + 持久化往返 -- ~40 行 - -### Phase T15-4: Bug 4 — dagre 图中心对齐到 (0,0) -- layoutNodesViaDagre 读 g.graph() {width,height},偏移每节点 -- Test: vitest 验证布局后图中心在原点附近 -- ~5 行 - -### Phase T15-5: Bug 5 — 画布拖线走 strategyConfig 管道 -- TopologyView 引入 ipcStrategyConfigGet/Put/Apply -- onConnect/onEdgesDelete 改写: 读 strategyConfig -> 更新 regionFilters -> put + apply -- sync() 也读 strategyConfig 的 region_filters -- Test: vitest 验证拖线调 strategyConfigPut 而非 platformUpdate -- ~60 行 - -### Phase T15-6: ADR-0036 + AGENTS.md 更新 -- ADR-0036 已写入 -- AGENTS.md 增加新 section 记录策略统一管道 - -### Phase T15-7: 全量门禁 + exe stage + push -- pnpm test / tsc --noEmit / i18n:check / pnpm build / cargo build --release / exe stage / chunk hash / codegraph sync / git push - -## 验收标准 - -1. 无平台选中 region 时 C 列不渲染订阅卡片 — vitest -2. CanvasControls icon 按钮 w-fit self-center — vitest -3. viewMode/locked 切换页面后恢复 + 重启后恢复 — vitest -4. 不闪现空态消息(P20 gate 保留)— vitest -5. dagre 布局后图中心在坐标系原点附近 — vitest -6. 画布拖线调 strategyConfigPut 而非直接 platformUpdate — vitest -7. 白盒配置文件能改策略且 GUI 同步 — vitest -8. 214+ vitest + tsc + i18n green - -## 依赖 - -- 无新依赖(复用已有 strategyConfig 管道 + dagre + zustand) - -## 联网调研结论 - -- ReactFlow 官方文档确认 dagre 布局后用 fitView 或手动计算 bounding box 居中 -- dagre wiki 确认节点 x,y 是中心坐标,graph.width/height 是整个图尺寸 -- 工业级 canvas toolbar 模式:segmented toggle 比 icon 按钮宽(Figma/VSCode 模式) -- Kiali Service Graph: 未绑定的节点不显示在画布上 diff --git a/docs/history/phases/GRILL_T15_CANVAS_V3_PLAN.md b/docs/history/phases/GRILL_T15_CANVAS_V3_PLAN.md deleted file mode 100644 index b9e46360..00000000 --- a/docs/history/phases/GRILL_T15_CANVAS_V3_PLAN.md +++ /dev/null @@ -1,111 +0,0 @@ -# GRILL T15 Canvas v3 Fix Plan - -> Source: grill session 2026-08-16 (Q1-Q4 user decisions all A/A1). -> Authoritative refs: ADR-0039 (canvas v3 fold/center/sync/two-file boundary), ADR-0036 (strategy write pipeline, extended here on the read side), docs/CONTEXT.md (domain glossary). -> Industry research: pplx kimi_k26 on (a) proxy/topology dual-fold toggle best practice, (b) dagre center-to-top-left canonical formula. Provenance archived in ADR-0039. - -## Scope (this plan only) - -Four user-visible issues, each with a root cause found in code (not speculation): - -1. **Region mode floods the canvas.** Toggle is useful, but RegionGroupNode is auto-expanding every per-node row. Fix: fold contract. -2. **Home button centers wrong.** `layoutNodesViaDagre` subtracts `graphLabel.width/2` on top of the correct `nodeWidth/2`; the offset grows with node count. Fix: drop the extra offset. -3. **Strategy badges mismatch the JSON.** `aClassLabel` is computed from `p.region_filters?.length` alone; only the `Region` variant renders correctly. `bClassLabel` is read from Resin `allocation_policy`, not from strategyConfig `b_class`. Fix: extend sync merge. -4. **Whitebox edits not visible from inside the GUI.** The strategy JSON path + reload button (mirroring the network-whitebox card) is missing. Fix: add it; keep two-file boundary. - -Out of scope: the legacy `patchAndSyncOnce` dead code (tracked in AGENTS SS56 ponytail-debt, not this plan); a JSON text editor inside the app; merging `config_export` with `strategy_config` into one file. - -## Phases - -### Phase T15-v3-1: RegionGroupNode fold contract - -- Mirror `SubscriptionGroupNode`: collapsed summary (region label uppercase + healthy/total count badge + region flag chip), no per-node rows by default. -- Expandable on click: same disclosure pattern as `SubscriptionGroupNode` (collapsible section with max 10 rows visible + "+N more"). -- **No auto-expand** when only one region exists; fold contract is stable regardless of count. -- i18n keys (add to all 18 locales): `topology.regionGroup` (label), `topology.expandNodes` (expand affordance), `topology.collapseNodes` (collapse affordance). Reuse `topology.healthy` / `topology.unhealthy` already present from T15-2. -- Test: vitest rendering a 50-node region group asserts exactly one summary card renders; expanding via the disclosure button adds at most 10 node rows. -- ~15 lines (component body) + 3 new keys x 18 locales. - -### Phase T15-v3-2: dagre center correction - -- In `layoutNodesViaDagre` (TopologyView.tsx:170-176), delete the two lines that compute `offsetX` / `offsetY` from `graphLabel.width` / `graphLabel.height` and the `... - offsetX` / `... - offsetY` terms in the node-position math. -- Result position: `{ x: pos.x - nodeWidth / 2, y: pos.y - nodeHeight / 2 }` only. Verified per ADR-0039 SS4 against the kimi_k26 research and the official ReactFlow + dagre example. -- Keep the existing `marginx: 20 / marginy: 20` graph config; dagre's internal margin still provides breathing room at the origin. -- Test: vitest with 1, 3, and 20 nodes each layout; assert the average node-position delta vs 1-node baseline is bounded (< graphLabel.width drift). Confirm Home (`setViewport({x:0,y:0,zoom:1})`) shows the graph origin corner consistently. -- ~2 lines deleted. - -### Phase T15-v3-3: strategyConfig read-side merge - -- Extend `PlatformFull` (TopologyView.tsx:34-42) with optional fields: `aClass?`, `bClass?`, `manualNodes?`, `subscriptionNames?`, `topN?`. -- In `sync()` (already merges `regions`), also merge these from `cfgRaw.platforms`: - - `aClass` <- `ps.a_class` (string; one of `manual` / `region` / `quality` / `subscription`) - - `bClass` <- `ps.b_class` (shell StrategyId string) - - `manualNodes` <- `ps.manual_nodes` - - `subscriptionNames` <- `ps.subscriptions` - - `topN` <- `ps.top_n` -- Replace the aClassLabel computation (L613) with a switch on `p.aClass ?? "manual"`: - - `manual` -> `t("topology.aClassManual")` - - `region` -> `t("topology.aClassRegion", { regions: p.region_filters!.join(",").toUpperCase() })` (unchanged) - - `quality` -> `t("topology.aClassQuality", { n: p.topN ?? 10 })` - - `subscription` -> `t("topology.aClassSubscription", { subs: (p.subscriptionNames ?? []).join(",") })` -- Replace bClassLabel computation (L612): use `p.bClass` (strategyConfig) if present; fall back to `mapResinToShell(p.allocation_policy)` only when `bClass` is absent (debug path). -- i18n keys (add to all 18 locales): `topology.aClassManual`, `topology.aClassQuality`, `topology.aClassSubscription`. Reuse `strategy.*` keys already present. -- Tests: - - Extend T15-5 "whitebox override" test: strategyConfig `a_class: "subscription"` + `subscriptions: ["sub1","sub2"]` and `a_class: "quality"` + `top_n: 5` each render the correct badge text (not "Manual"). - - New test: assert Home centers the graph by calling `layoutNodesViaDagre` with 1 vs 20 nodes; assert `setViewport({x:0,y:0,zoom:1})` arguments match (this is covered by T15-v3-2 actually; here only assert the pre-Home layout positions are stable). -- ~35 lines (sync merge expansion + label switch) + 3 new keys x 18 locales. - -### Phase T15-v3-4: Settings strategy-JSON reload surface - -- Add a "Strategy config" card in `SettingsView.tsx`, mirroring the existing "Network whitebox" card pattern: - - read-only path label: `egressapikey-strategy.json` absolute path (need a new IPC `strategy_config_path` or compute from `get_config_dir` + filename; simplest is to reuse `get_config_dir` and append the filename in TS). - - "Reload from disk" button: calls `ipcStrategyConfigGet` -> `ipcStrategyApply` (forces re-read + re-PROTECT). Reuses existing IPC, no new Rust code. - - Status line after reload: applied N platforms, errors M (parsed from `strategy_apply` result). -- No text editor widget, no Save button on this card. External-edits then reload is the whitebox loop (ADR-0039 SS5 / SS1). -- i18n keys (add to all 18 locales): `settings.strategyConfigPath`, `settings.strategyConfigReload`, `settings.strategyReloaded`. -- Test: vitest rendering SettingsView with mocked `strategy_config_get` returning a 2-platform config; click reload -> assert `strategy_apply` is invoked exactly once. Reuses the existing SettingsView test file. -- ~12 lines + 3 new keys x 18 locales. - -### Phase T15-v3-5: Full gate + exe hard close-loop + push - -- `pnpm test --run` (verify 225 baseline + new tests still green) -- `pnpm exec tsc --noEmit` -- `pnpm i18n:check` (verify all 18 locales match; new keys added symmetrically) -- `pnpm build` (Vite writes a new chunk hash to `dist/assets`) -- `cargo build --release -p egressapikey-app --features custom-protocol` -- Stage `release/windows-gui/EgressAPIKEY.exe` + `resin.exe` sidecar -- **Chunk-hash hard close-loop** (AGENTS SS5): grep the new Vite chunk hash inside the exe bytes via `Buffer.indexOf`; FAIL if not found. -- Smoke: launch staged exe once, assert `MainWindowTitle == "EgressAPIKEY"`, WS ~30-40MB, resin.exe child alive. -- `codegraph sync .` -- `git add` changed source + locale + docs + AGENTS.md update; `git push origin codex/rust-port`. - -### Phase T15-v3-6: AGENTS.md SS57 + ADR-0036 read-side addendum - -- Append AGENTS.md SS57 documenting: fold contract, dagre center correction, strategy badge sync merge (ADR-0036 read-side), two-file boundary, reload surface, new i18n keys, test totals. -- Append a short addendum to ADR-0036 noting that the *read* side of the pipeline is now specified by ADR-0039 SS2; no reversal of the write-side decision. - -## Acceptance Criteria - -1. Region viewMode renders folded summary cards only; 50-node region group renders 1 card + 0 node rows by default - vitest. -2. `setViewport({x:0,y:0,zoom:1})` frames the same origin corner with 1 node and with 20 nodes - vitest asserting layout positions do not drift by more than the natural dagre rank growth. -3. A `subscription` A-class strategy renders `A: Subscription: sub1,sub2` (i18n key), not `A: Manual` - vitest. -4. A `quality` A-class strategy renders `A: Quality Top-10` (i18n key), not `A: Manual` - vitest. -5. Settings shows `egressapikey-strategy.json` path + a Reload button that invokes `strategy_apply` once - vitest. -6. 225+ vitest + tsc + i18n:check green; new keys x 18 locales; cargo untouched; release exe staged with chunk-hash verified; codegraph synced; pushed. - -## Dependencies - -- No new npm or cargo deps. All edits use existing ReactFlow/dagre/zustand/i18next plumbing. -- ctx_* file-edit pattern continues (Node `.cjs` scripts under `.codex-tmp/`); AGENTS SS7 windows-file-integrity observed (LF, no BOM, no inline `$` PS). - -## Risk and rollback - -- ADR-0039 SS4 (dagre offset deletion) is the only change that touches layout math; visual regression risk is minimal (Home now reliably shows the origin corner instead of a node-count-dependent point). Roll-back: re-add the deleted two lines. -- ADR-0039 SS3 (fold) is additive (node rows are now hidden by default; expanding still works). Roll-back: change the default expanded state on RegionGroupNode. -- ADR-0039 SS2 (badge sync) is additive; fallback to Resin fields when strategyConfig has no entry for the platform. Roll-back: revert the sync merge lines. - -## Industry references - -- pplx kimi_k26 (2026-08-16): "proxy/topology dual-fold toggle best practice" - conclusion: mature tools keep the toggle but default to a fold-first mode; region mode should never auto-expand child rows. -- pplx kimi_k26 (2026-08-16): "dagre center to ReactFlow top-left authoritative formula" - conclusion: `pos.x - nodeWidth/2` is the canonical conversion; graph.width/2 subtraction is a known mistake. -- ADR-0039 captures both citations inline. diff --git a/docs/history/phases/GRILL_T15_PERFORMANCE_FIX.md b/docs/history/phases/GRILL_T15_PERFORMANCE_FIX.md deleted file mode 100644 index 1b5fd039..00000000 --- a/docs/history/phases/GRILL_T15_PERFORMANCE_FIX.md +++ /dev/null @@ -1,118 +0,0 @@ -# GRILL T15 — Performance Fix Branch (Post-T14 Remaining Issues) - -> **Branch**: codex/rust-port (parallel fix branch) -> **Base commit**: 8dac8e6 (after T15 canvas bugfix) -> **Status**: All Q1-Q10 decisions user-approved. Plan ready for execution. -> **Ponytail**: full mode. - -## Decisions (Q1-Q10) - -| # | Area | Decision | Template Reference | -|---|------|----------|-------------------| -| Q1 | Sidecar process leak test | (A) Real process smoke test + ctrlc signal handling | Zed PR#58885, crynta/terax-ai job.rs | -| Q2 | Lightweight mode tray entry | (A) Menu item dynamic toggle (Normal/Entering/InLightweight) | clash-verge-rev tray mode | -| Q3 | Ctrl+C signal handling | (A) ctrlc crate + ShutdownFlag, cross-platform | cargo job.rs, kimi research | -| Q4 | 7 perf domains all done | (A) All 9 subtasks in one pass | kimi + exa research | -| Q5 | reqwest::Client reuse | (A) OnceLock inside ResinClient | clash-verge-rev commit bfe2471 | -| Q6 | Startup lazy loading | (A) All 6 Views lazy + Suspense | psysonic PR#249 | -| Q7 | Backend sync migration | (A) Only migrate DiagnosticsView to usePoll | -- | -| Q8 | Runtime log level control | (A) set_log_level IPC + EnvFilter reload + Settings dropdown | clash-verge-rev log.rs | -| Q9 | Canvas perf (React.memo + throttle) | (A+B) React.memo on 4 custom node components + onMove rAF throttle | ReactFlow perf guide | -| Q10 | Test strategy | (A) Diff-level closed-loop: 3 cargo + 3 vitest = 6 tests | AGENTS S4 | - -## Execution Plan (5 Phases) - -### T15-1: DiagnosticsView to usePoll (Q7) - -**Root cause**: DiagnosticsView (499 lines) still uses hand-rolled setInterval + document.hidden check (lines 65-122). usePoll hook exists in src/hooks/usePoll.ts and already handles visibility pause/resume + AbortController cleanup. This is the only View not using usePoll. - -**Changes**: -- File: src/views/DiagnosticsView.tsx — remove intervalRef (L65), two useEffect blocks (L68, L104), visibilitychange listener (L116-122). Replace with usePoll(10000, refreshDiagnostics, { fireImmediately: true }). -- File: src/views/DiagnosticsView.test.tsx — add 1 vitest: verify document.hidden pauses polling. - -**Validation**: vitest pass + tsc green - -### T15-2: Runtime log level control (Q8) - -**Root cause**: tauri_plugin_tracing Builder at main.rs L60-67 uses with_max_level(LevelFilter::INFO) — hardcoded, no runtime reload. - -**Changes**: -- File: src-tauri/src/main.rs — store tracing reload handle. The tauri_plugin_tracing plugin may not expose its internal subscriber reload handle. Approach: check if the plugin exposes a reload handle; if not, build a custom tracing_subscriber with EnvFilter::reload and store in State<LogLevelHandle>. Fallback: add an ArcAtomicU8 log level gate that wraps tracing macros. -- File: src-tauri/src/commands/mod.rs — new set_log_level(level: String) IPC command. Validate level in {error,warn,info,debug}. Apply to reload handle or atomic gate. -- Register in main.rs invoke_handler: commands::set_log_level. -- File: src/lib/ipc.ts — new ipcSetLogLevel(level) wrapper. -- File: src/views/SettingsView.tsx — dropdown in Settings (error/warn/info/debug), persisted to settings.json#logLevel. -- i18n: add settings.logLevel + settings.logLevelError/Warn/Info/Debug keys across 18 locales. - -**Tests**: -- 2 cargo: set_log_level param validation (invalid rejected, valid accepted) + level enum round-trip. -- 1 vitest: Settings dropdown change calls invoke("set_log_level", { level: "warn" }). - -**Validation**: cargo test pass + vitest pass + tsc green + i18n:check green - -### T15-3: Canvas perf — React.memo + onMove throttle (Q9 A+B) - -**Root cause**: PlatformNode (L214), RegionGroupNode, SubscriptionGroupNode, EntryPortNode are plain function components with no React.memo. Every store state change re-renders all nodes. onMove is not bound (only onMoveEnd is at L542) — so the throttle part may be a no-op if onMove is already absent. - -**Changes**: -- File: src/views/TopologyView.tsx -- Wrap all 4 custom node components with React.memo: MemoPlatformNode = React.memo(PlatformNode), same for others. -- Update nodeTypes map (L351) to use memoized versions. -- Check if onMove is bound: if not, skip rAF throttle (no work needed). If yes, gate state updates with requestAnimationFrame. -- Add areEqual custom comparator if shallow props comparison is insufficient (data object may be new each render). - -**Tests**: -- 2 vitest: (1) React.memo — pass same data object reference, verify component render count = 1. (2) onMove (if bound) — verify no state update beyond rAF budget. - -**Validation**: vitest pass + tsc green + pnpm build green (chunk size should not regress) - -### T15-4: reqwest::Client reuse (Q5) - -**Root cause**: ResinClient::new() creates a new reqwest::Client per call. IPC layer constructs a fresh ResinClient for each command, building a new TLS pool + connection pool each time. - -**Changes**: -- File: crates/resin-core/src/resin_client.rs -- Add static CLIENT: OnceLock<reqwest::Client> = OnceLock::new() + fn shared_client() -> and use it in ResinClient::new(). -- No API change — callers see the same ResinClient interface. - -**Test**: 1 cargo — verify shared_client() returns same instance (pointer eq). - -**Validation**: cargo test pass + cargo build green - -### T15-5: Build + test + smoke + i18n + doc + push (Q10 final) - -**Changes**: -- pnpm build -> note chunk hash -- cargo build --release -p egressapikey-app --features custom-protocol -- Stage exe to release/windows-gui/ -- Grep chunk hash in exe bytes -- Smoke: MainWindowTitle + WorkingSet + resin child alive -- pnpm i18n:check all 18 locales -- cargo test -p resin-core --lib -- pnpm test -- New ADR: docs/adr/0037-t15-perf-fixes.md -- Update docs/GRILL_T15_PERFORMANCE_FIX.md (this file) -- Update AGENTS.md with T15 section -- Commit + push - -**Validation**: ALL green - -## Dependencies: NONE new -- React.memo: React 19 stdlib -- requestAnimationFrame: browser stdlib -- OnceLock: Rust stdlib -- tracing reload: tracing_subscriber (already via tauri-plugin-tracing) - -## Risk Assessment -- T15-1: Low — replacing setInterval with usePoll, proven pattern from T14-3 -- T15-2: Medium — tracing reload handle may not be exposed by tauri_plugin_tracing; fallback is atomic gate -- T15-3: Low — React.memo is non-breaking -- T15-4: Low — OnceLock is non-breaking -- T15-5: Standard build+smoke pipeline - -## Completion Criteria -- [x] T15-1: DiagnosticsView uses usePoll, 1 vitest pass (DONE) -- [x] T15-2: set_log_level IPC works, 2 cargo + 1 vitest pass, i18n keys added (DONE) -- [x] T15-3: 4 custom node components wrapped in React.memo, 2 vitest pass (DONE) -- [x] T15-4: OnceLock<Client> in resin_client.rs, 1 cargo pass (DONE) -- [x] T15-5: ALL green (cargo test=120, vitest=222, tsc clean, i18n 18 locales 332 keys, smoke exe 36.8MB+resin 50.1MB, chunk hash verified, push) (DONE) \ No newline at end of file diff --git a/docs/history/phases/GRILL_T16_CANVAS_REGION_FILTER_PLAN.md b/docs/history/phases/GRILL_T16_CANVAS_REGION_FILTER_PLAN.md deleted file mode 100644 index 585a4859..00000000 --- a/docs/history/phases/GRILL_T16_CANVAS_REGION_FILTER_PLAN.md +++ /dev/null @@ -1,53 +0,0 @@ -# GRILL_T16 Canvas Region Filter + Port Drag-Bind + MiniMap i18n - -> ADR-0040: 0040-canvas-region-filter-port-bind-minimap.md -> Fixed point: 04929a4 (T15-v3-audit) -> Grilling: Q1=A, Q2=A+port-drag, Q3=A+tooltip-i18n - -## Phase T16-1: Region viewMode filter guard - -Root cause: TopologyView.tsx L737-756 region viewMode iterates all subGroups nodes and generates region group cards for every region that has nodes, without checking selectedRegions. - -Fix: In the if (viewMode === "region") block, add a guard: if (!selectedRegions.has(region)) continue; before pushing the regiongroup node. - -File: src/views/TopologyView.tsx L737-756 -Lines: ~3 -Test: vitest render TopologyView with mocked platforms region_filters=["jp"], mock subGroups with nodes in jp+us+kr, assert only regiongroup-jp appears. - -## Phase T16-2: Port drag-bind onConnect - -Root cause: onConnect (L812) only handles conn.source.startsWith("platform-"). Dragging from entry-port to platform is silently ignored. - -Fix: Add new branch at top of onConnect for entry-port- to platform- connections, calling ipcPortBindPlatform(port, platformName) then sync(). - -File: src/views/TopologyView.tsx L812 onConnect -Lines: ~12 -Test: vitest mock ipcPortBindPlatform, simulate onConnect source="entry-port-1790" target="platform-Default", assert ipcPortBindPlatform called with (1790, Default) and sync called. - -## Phase T16-3: MiniMap i18n tooltip + nodeColor - -Fix: Wrap MiniMap in div with i18n title, add nodeColor function coloring nodes by type (entryPort=blue, platform=purple, subscriptionGroup=green, regionGroup=amber), add maskColor. - -New i18n key: topology.minimapHint x18 locales. - -File: src/views/TopologyView.tsx L925 MiniMap section -Lines: ~15 -Test: vitest render TopologyView, assert MiniMap wrapper div has title attribute with translated text. - -## Phase T16-4: Canvas open strategy config entry - -Fix: Add button near CanvasControls that opens strategy config file in OS default editor. Reuses ipcGetConfigDir + egressapikey-strategy.json filename + openPath. - -New i18n key: topology.openStrategyConfig x18 locales. - -File: src/views/TopologyView.tsx CanvasControls area -Lines: ~10 -Test: vitest render TopologyView, click open-config button, assert ipcGetConfigDir and openPath called. - -## Phase T16-5: Full gate + exe closed-loop - -pnpm test >=228 pass (was 225, +3 new), tsc green, i18n:check 343 keys, pnpm build green, cargo build release green, stage exe, verify chunk hash, smoke, codegraph sync, git commit + push. - -## Phase T16-6: AGENTS.md SS58 + CONTEXT.md update - -AGENTS.md: add section 58 documenting 3 fixes. CONTEXT.md: add terms selectedRegions, port_bind_platform, viewMode, MiniMap. \ No newline at end of file diff --git a/docs/history/phases/GRILL_T16_PORT_IDENTITY_PLAN.md b/docs/history/phases/GRILL_T16_PORT_IDENTITY_PLAN.md deleted file mode 100644 index ed464e17..00000000 --- a/docs/history/phases/GRILL_T16_PORT_IDENTITY_PLAN.md +++ /dev/null @@ -1,64 +0,0 @@ -# GRILL T16 - Port-Based Identity 复核收尾(基于真实 tree 修订) - -> 原 7 Phase 路线图经 ctx 代码级核实后修订:T16-1/4/5 已在历史 commit 落地; -> T16-2 route_id/normalize_auth 已被 commit c71bab5 P1 主动删除; -> T16-3 白盒配置闭环已被 T15 ADR-0036 strategyConfig 管道完成。本计划收尾 = 文档校正 + ADR-0037 + 门禁 push。 - -## 前序(已验证事实) - -- T15 commit 8dac8e6: 7 Phase + 219 vitest + tsc + i18n(327 keys) + cargo(119 pass) + exe stage + push -- ADR-0036 strategyConfig 统一管道已落地 (strategy_config_get/put/apply Rust IPC + ipcStrategyConfigGet/Put TS) -- ADR-0037 决策记录:port-based identity 已全面落地,route_id/interceptor 已被 P1 commit c71bab5 主动删除并不复接回 - -## T16 原 7 Phase 实际状态 - -| Phase | 原计划 | 实际状态 | 证据 | -|---|---|---|---| -| T16-1 | TopologyView A 列从单一节点改为多端口节点 | ✅ 已落地 | TopologyView.tsx L572-579 ports.forEach + 测试 L236 "ADR-0012: A->B edge connects port to platform" | -| T16-2 | route_id helper 接入 IPC 入口 | ❌ 不复接回 | commit c71bab5 P1 主动删除("Delete interceptor.rs, a4_3_live.rs, route_id/normalize_auth, InterceptorPort"),port-based identity 取代三个组身份模型 | -| T16-3 | 白盒配置全程闭环验证 | ✅ 已落地 | T15 ADR-0036 strategyConfigPut -> strategyApply 管道 + TopologyView onConnect/onEdgesDelete 走该管道;T15-5 测试覆盖 | -| T16-4 | PlatformsView 左 pane 重构为 Entry Ports | ✅ 已落地 | PlatformsView.tsx L390 t("platform.entryPorts") + L429 label + platform_name/unbound 显示,KeyCandidates 概念已删除 | -| T16-5 | ProcessRouteView process->port | ✅ 已落地 | ProcessRouteView.tsx L30 r.target_port + L50 processRoute.conflict { port: tgt } + L84 processRoute.targetPort;Rust process_route_add L407-425 target_port 字段 + process_route_conflict_check 基于 port | -| T16-6 | ADR-0037 + AGENTS.md | ✅ 本计划收尾 | ADR-0037 写入 + AGENTS.md §28/§30 Reverted 块 | -| T16-7 | 全量门禁 + exe stage + push | ✅ 本计划收尾 | 纯文档变更跑 pnpm test/i18n:check/tsc 不跑 cargo build --release | - -## 收尾执行计划 - -### Phase T16-Final-1: ADR-0037 ✅ 完成 -- docs/adr/0037-port-based-identity-reverts-route-id.md (2047 bytes, UTF-8 LF no BOM) -- 决策:port-based identity 已全面落地,route_id/interceptor 不复接回 - -### Phase T16-Final-2: AGENTS.md §28/§30 文档失步校正 ✅ 完成 -- §28 (L351): Reverted by P1 (commit c71bab5) 块前置,注明 route_id/normalize_auth 已删除 -- §30 (L365): Reverted by P1 (commit c71bab5) 块前置,注明 interceptor.rs 整个模块已删除 -- 字符纯净:BEL=0 CR=0 LF=668,无字面 \r/\n -- §541 那个 §30 (P5+P6 route-correction final architecture) 是正确描述,未动 - -### Phase T16-Final-3: 全量门禁 -- pnpm test (vitest) — 219 pass / 16 files green -- pnpm i18n:check — 327 keys / 18 locales green -- pnpm exec tsc --noEmit — 0 errors -- 纯文档变更不必 cargo build --release(不改任何 .rs source) - -### Phase T16-Final-4: commit push -- 分阶段提交或合并提交(ADR-0037 + AGENTS.md + PLAN.md) -- codegraph sync . 重索引(.md 变更也跑避免 codegraph 漂移) -- git push 到 codex/rust-port - -## 验收标准 - -1. ADR-0037 文件存在且 UTF-8 LF no BOM ✅ -2. AGENTS.md §28/§30 Reverted 块文案干净无 BEL/backslash 字符 ✅ -3. GRILL_T16_PORT_IDENTITY_PLAN.md 状态表与真实 tree 对齐 ✅ -4. 219 vitest + tsc + i18n:check 仍 green -5. commit + push 完成 - -## 依赖 - -- T15 ADR-0036 strategyConfig 管道(已落地) -- T15 commit 8dac8e6(基线) -- P1 commit c71bab5 死代码删除决策(已发生) - -## 联网调研方向 - -无新增——本计划是事实校正收尾,不引入新功能。如有未来 agent 想复活 route_id/interceptor,应以 ADR-0037 + 本计划状态表为依据否决。 \ No newline at end of file diff --git a/docs/history/phases/GRILL_T17_CANVAS_V4_PLAN.md b/docs/history/phases/GRILL_T17_CANVAS_V4_PLAN.md deleted file mode 100644 index abe0c018..00000000 --- a/docs/history/phases/GRILL_T17_CANVAS_V4_PLAN.md +++ /dev/null @@ -1,109 +0,0 @@ -# GRILL_T17 Canvas V4 — Node Pool + Toolbar Merge + MiniMap ariaLabel + Cross-subscription Dedup - -> ADR-0041: [0041-canvas-v4-node-pool-toolbars-merge.md](./adr/0041-canvas-v4-node-pool-toolbars-merge.md) -> Fixed point: 74c90ca (T16-audit) -> Grilling: Q1=C (node unified pool), Q2=C (Kiali-style one-node-one-group), Q3=A (merge button into toolbar), Q4=B (8 tests full coverage) -> Research sources: D:/Aworker/EgressAPIKEY/.codex-tmp/ReactFlow dagre layout multi-column topo.md + D:/Aworker/EgressAPIKEY/.codex-tmp/Tauri ReactFlow Minimap Localized Toolti.md - -## Root causes (from code diff) - -| Bug | Root cause | File:Line | -| --- | --- | --- | -| 1. Entry-port column stray nodes in region viewMode | subGroups.forEach loop L697-731 is unconditional; in region viewMode those nodes have no edges and dagre places them in free space near entry-port column | TopologyView.tsx:697-731 | -| 2. openStrategyConfig button overlaps MiniMap | Standalone absolute button at bottom-20 left-4 sandwiched between CanvasControls (bottom-2) and MiniMap (bottom-0) | TopologyView.tsx:942-954 | -| 3. MiniMap hover tooltip not i18n-realized on hover | div[title] wrapper L955-972 has zero height because MiniMap renders into an absolutely positioned child; native title hit-test fails | TopologyView.tsx:955-972 | -| 4. Duplicate node rows across subscriptions | region viewMode loops subGroups*g.nodes without dedup by node_hash; subscription viewMode nodeRows map also lacks explicit dedup | TopologyView.tsx:743-751, 718-726 | - -## Phase T17-1: viewMode guard on subGroup loop (S1) - -Root cause: subGroups.forEach L697 unconditionally pushes subscriptionGroup nodes even in region viewMode; they become edgeless and dagre scatters them. - -Fix: add `if (viewMode !== "subscription") return;` early-return as the first line inside the forEach callback body (before computing healthLabel / sub / filteredNodes). This forces exactly one C-column aggregation layer per viewMode. - -File: src/views/TopologyView.tsx L697 (inside subGroups.forEach((g) => { ... }) body) -Lines: ~1 -Test: vitest render TopologyView in region viewMode with mocked subGroups; assert node with id="subgroup-X" is NOT present in the node list, only "regiongroup-*" nodes. - -## Phase T17-2: Cross-subscription node dedup by node_hash (S2) - -Root cause: L743-751 `for (const g of subGroups) for (const n of g.nodes) entry.nodeRows.push(...)` copies nodes without checking `seen.has(n.node_hash)`. L718-726 subscription nodeRows map has the same structural issue inside one subscription group when Resin echoes proxies. - -Fix: -- Region viewMode: maintain `const seen = new Set<string>()` outside the subGroups loop; inside `for (const n of g.nodes)` guard with `if (n.node_hash && seen.has(n.node_hash)) continue; seen.add(n.node_hash);` BEFORE incrementing entry.total / entry.healthy / pushing to entry.nodeRows. -- Subscription viewMode: maintain the same `const seen = new Set<string>()` for each `g.nodes` iteration; skip duplicate node_hash within one subscription group. - -File: src/views/TopologyView.tsx L718-726 (subscription path) + L743-751 (region path) -Lines: ~8 -Test 1: region viewMode with two subscriptionGroups both containing node_hash="abc" -> regionRows has exactly 1 row with that display_tag (not 2). -Test 2: subscription viewMode with one subscriptionGroup containing duplicate node_hash "abc" twice -> subgroup nodeRows has 1 row (not 2). - -## Phase T17-3: MiniMap ariaLabel replaces div[title] (S3) - -Root cause: L955-972 wraps MiniMap in static div[title] which has height 0 due to absolute-positioned child MiniMap panel. The native title tooltip never fires hit-test. - -Fix: delete the wrapper div. Pass `ariaLabel={t("topology.minimapHint")}` prop directly to `<MiniMap>` — xyflow 12.11.2 source renders it into an SVG `<title>` which is both the accessibility name and the native hover tooltip (verified in .codex-tmp research file). - -File: src/views/TopologyView.tsx L955-972 -Lines: -18/+2 (net -16) -Test 1: render TopologyView; assert MiniMap has aria-label prop equal to translated "topology.minimapHint" text. -Test 2: assert no <div title=...> wrapper around MiniMap in the rendered output (anti-regression). - -## Phase T17-4: openStrategyConfig merged into CanvasControls toolbar (S4) - -Root cause: L942-954 standalone absolute button at bottom-20 left-4 occludes CanvasControls hover region and MiniMap. - -Fix: delete the standalone button block. Add a new button row inside CanvasControls (L478-509) with title={t("topology.openStrategyConfig")} and the same onClick body. Place it as the last item in the second row (below zoom/fit group) or to the right of the viewMode segmented control — whichever fits the existing flex layout without reflow. - -File: src/views/TopologyView.tsx L942-954 (delete) + L478-509 (insert into CanvasControls) -Lines: -13/+15 (net +2) -Test 1: render TopologyView; query the canvas-controls container; assert there IS a button with title="Open strategy config" inside it. -Test 2: assert NO standalone button with absolute bottom-20 left-4 position remains (anti-regression). - -## Phase T17-5: Full gate + exe closed-loop - -Run order: - -1. `pnpm test` => expect 233 pass (225 baseline + 8 new). No regressions. -2. `npx tsc --noEmit` green. -3. `node scripts/i18n-check.cjs` => expect "343 keys / 18 locales" (no new keys). -4. `pnpm build` green. Note the Vite chunk hash. -5. `cargo build --release -p egressapikey-app --features custom-protocol` green. -6. Copy exe to `release/windows-gui/EgressAPIKEY.exe`. -7. Verify the new Vite chunk hash appears in the staged exe bytes (ascii grep). -8. Smoke: Start-Process exe, assert MainWindowTitle="EgressAPIKEY", WS ~35MB, resin.exe child alive at ~50MB. -9. `codegraph sync .` re-index changed files. -10. git add + commit + push. - -## Phase T17-6: Documentation + ADR - -- Write ADR-0041 (done above). -- Update docs/CONTEXT.md: add `nodePool` + `aggregationColumn` terms (the C-column concept under Kiali-style invariant). -- Add AGENTS section §59 documenting T17 fix. -- Verify docs/ADR-0040 references updated to note partial supersession by ADR-0041 (S3 + S4). - -## Test plan (8 tests, Q4=B confirmed) - -| # | Name | Phase | Coverage | -| --- | --- | --- | --- | -| 1 | T17-1: region viewMode excludes subGroup nodes (happy) | T17-1 | S1 guard happy path | -| 2 | T17-1: subscription viewMode still renders subGroup nodes (boundary/regression) | T17-1 | viewMode does not break the other branch | -| 3 | T17-2: region viewMode dedups by node_hash across subscriptions (happy) | T17-2 | S2 dedup happy | -| 4 | T17-2: subscription viewMode dedups duplicate node_hash within one subscription (boundary) | T17-2 | dedup on the other path | -| 5 | T17-3: MiniMap renders ariaLabel translated text (happy) | T17-3 | S3 ariaLabel path | -| 6 | T17-3: no div[title] wrapper around MiniMap (anti-regression) | T17-3 | ensures old antipattern gone | -| 7 | T17-4: CanvasControls contains openStrategyConfig button (happy) | T17-4 | S4 toolbar merge | -| 8 | T17-4: no standalone absolute bottom-20 left-4 button remains (anti-regression) | T17-4 | ensures standalone removed | - -Total: 225 + 8 = 233 pass (was 225 after T16). - -## Verification criteria (acceptance) - -- Vitest 233 pass / 16 files. -- tsc --noEmit green. -- i18n:check 343 keys / 18 locales (unchanged). -- pnpm build green; new chunk hash embedded in exe. -- cargo build --release green. -- Exe smoke: MainWindowTitle=EgressAPIKEY, resin.exe child alive, no stderr. -- codegraph sync green. -- git push to codex/rust-port with the commit. -- End-to-end manual check: switch region <-> subscription viewMode; confirm no stray subGroup cards under entry-port column in region viewMode; confirm MiniMap hover shows localized tooltip; confirm openStrategyConfig button sits inside the left toolbar group, not a standalone floating pill. diff --git a/docs/history/phases/GRILL_T17_HEADLESS_SERVER.md b/docs/history/phases/GRILL_T17_HEADLESS_SERVER.md deleted file mode 100644 index 193177d2..00000000 --- a/docs/history/phases/GRILL_T17_HEADLESS_SERVER.md +++ /dev/null @@ -1,99 +0,0 @@ -# GRILL T17 — Headless NPM Server Fix Branch - -> Date: 2026-08-17 -> Status: PLANNED (awaiting user execution signal) -> Branch: codex/rust-port (T17 headless fix branch, parallel with other fix branches) -> Related ADRs: [ADR-0043](adr/0043-headless-server-build-separation.md) -> Sources: atomcode research T17-Q1 (12 searches, 11 web_fetch, dual-binary architecture), T17-Q2 (20 sources, dual-runtime frontend adaptation), T17-Q3 (16 sources, build/packaging separation), T17-Q4 (18 sources, enterprise documentation structure) - -## Context - -The project ships a Tauri 2 desktop GUI (EgressAPIKEY.exe) and a headless HTTP server binary (egressapikey-headless.exe) from the same codebase. The headless binary serves the prebuilt React dist/ via axum ServeDir and reverse-proxies /api/v1/* and /metrics/* to the Resin Go sidecar with admin bearer token injection. This is the "npm install" path for users who want localhost access without the Tauri desktop shell. - -### Current state (verified) - -- `src-tauri/src/bin/headless.rs` — ~230 lines, fully functional: axum build_router + ServeDir + proxy_to_resin + boot_resin_standalone + two-phase shutdown + tracing daily rotation + dirs crate OS-standard paths -- `Cargo.toml` [[bin]] name="egressapikey-headless" path="src/bin/headless.rs" — **NO required-features gating** (Tauri bundler auto-discovery hazard, rssh issue + tauri #15325) -- `release/windows-gui/npm-server/egressapikey-headless.exe` — headless artifact nested inside GUI channel directory (should be separate release/windows-backend/) -- `release/windows-backend/` — only has egressapikey-headless.d (1591 bytes debug symbol), NO release exe staged -- `build-all.ps1` L33-35 — headless build failure marked as "non-fatal, GUI-only" -- `npm/launcher.cjs` — ~80 lines, CommonJS, EGRESSAPIKEY_HEADLESS_BIN + EGRESSAPIKEY_DIST path resolution (codeg CODEG_STATIC_DIR/CODEG_MCP_BIN isomorphic) -- `npm/package.json` — @egressapikey/server, bin: egressapikey-server, engines node>=18 -- AGENTS.md S5 — stale: "Backend-only headless target = cargo build --release -p resin-core" (actual headless is src-tauri crate bin) -- CI ci.yml backend job — builds resin-core per-OS, NOT egressapikey-headless binary -- No headless/deployment/npm/server/docker documentation in docs/ - -## Grill decisions (4 questions, all resolved) - -| Q | Decision | Industry template | Key evidence | -|---|---|---|---| -| Q1 | **B** — verify end-to-end first, store mental model to wiki, then add docs + VPS closure | headless.rs already has VPS-ready axum + Go sidecar + ServeDir + reverse proxy | atomcode T17-Q1: codeg-server, rssh-server, emem systemd unit | -| Q2 | **A** — frontend ipc.ts dual-mode adapter (isTauri() + fetch fallback) | codeg port-adapter + ChatGPT-Next-Web TransformStream for SSE | atomcode T17-Q2: 20 sources, Wails v3 maintainer confirms WebView IPC is security-disabled for remote URLs | -| Q3 | **A** — full separation (source move + required-features + release dir migrate + CI split job) | codeg build-tauri + build-server separate jobs; rssh source relocation | atomcode T17-Q3: 16 sources, tauri issue #15325 stage-2 disk scan bug, PR #15427 fix | -| Q4 | **A** — add maintenance docs (Diataxis 4 quadrants) + sync all grill docs/README/comments | Diataxis framework + codeg docs + mihomo wiki + code-server npm.md | atomcode T17-Q4: 18 sources | - -## Execution plan (3 phases, 10 steps) - -### Phase 1: End-to-end verification (Q1=B) - -| Step | Action | Acceptance criteria | -|---|---|---| -| P1 | Launch egressapikey-headless.exe + dist/ + resin sidecar. Verify (a) browser loads React SPA at http://127.0.0.1:14200, (b) /api/v1/* proxies to Resin sidecar returns real data, (c) Ctrl+C two-phase shutdown kills resin child | All three verified with evidence. Mental model stored to wiki. | - -### Phase 2: Build separation (Q3=A) - -| Step | Action | Acceptance criteria | -|---|---|---| -| P2 | Move headless.rs from src-tauri/src/bin/headless.rs to src-tauri/src/headless_main.rs. Cargo.toml [[bin]] path="src/headless_main.rs" + required-features=["headless"]. Add [features] headless = [] | cargo build --bin egressapikey-headless --features headless succeeds. cargo build (no features) does NOT compile headless. | -| P3 | Migrate release artifact from release/windows-gui/npm-server/ to release/windows-backend/. Self-contained: headless exe + dist/ + resin.exe sidecar | release/windows-backend/ contains egressapikey-headless.exe + dist/ + resin.exe | -| P4 | build-all.ps1 + build-all.sh: headless build failure changes from non-fatal warning to fatal exit 1 | Script exits non-zero if headless build fails | -| P5 | CI ci.yml: backend job changes from resin-core per-OS binary to egressapikey-headless binary (cargo build --release -p egressapikey-app --bin egressapikey-headless --features headless). Independent rust-cache key. Artifact: <os>-backend.tar.gz | CI backend job produces egressapikey-headless binary per platform | - -### Phase 3: Frontend adaptation + documentation (Q2=A, Q4=A) - -| Step | Action | Acceptance criteria | -|---|---|---| -| P6 | src/lib/ipc.ts: each wrapper detects isTauri(). Tauri: invoke(). Browser: fetch("/api/v1/...") direct to headless axum reverse proxy. SSE: TransformStream reassembly (ChatGPT-Next-Web pattern) | vitest: isTauri()=true -> invoke, isTauri()=false -> fetch. All wrappers dual-mode. | -| P7 | New docs: npm/README.md (rewrite per code-server template: install->URL->credentials 3-line closure + CLI flags table + env overrides + security model). docs/HEADLESS_DEPLOYMENT.md (systemd unit template + env table + port config + dist path + sidecar placement, per mihomo/vaultwarden). docs/HEADLESS_RUNBOOK.md (log location + /healthz + graceful shutdown + restart policy + troubleshooting) | 3 docs files created with industry-standard content per atomcode Q4 research | -| P8 | AGENTS.md sync: S5 CI/CD backend job description, S17 G5 backend matrix, all headless references aligned to new path release/<os>-backend/ and egressapikey-headless bin name | grep headless/backend/npm-server in AGENTS.md shows no stale references | -| P9 | README.md + README_CN.md sync: add headless server install/usage section (dual-target product explanation + npm install one-liner + CLI flags link) | Both READMEs have headless section | -| P10 | Test closed-loop: vitest verifies ipc.ts dual-mode (isTauri branch + fetch branch). cargo build --bin egressapikey-headless --features headless verifies required-features gating. launcher.cjs --dry-run verifies path resolution | 3 test categories green. No false positives. | - -## Industry templates referenced (from atomcode research) - -- **codeg** (docs.codeg.app): one Cargo workspace, shared codeg_lib, 3 binaries (codeg/codeg-server/codeg-mcp), CI build-tauri + build-server separate jobs, CODEG_STATIC_DIR/CODEG_MCP_BIN env pattern -- **rssh** (github.com/shihuili1218/rssh): one crate, 3 binaries (rssh/rssh-cli/rssh-server), required-features gating + source relocation (avoid src/bin/ auto-discovery), include_dir embedded dist for headless -- **clash-verge-rev**: Tauri GUI + Go sidecar (mihomo), prebuild.mjs sidecar download, externalBin tauri.conf.json -- **ChatGPT-Next-Web**: stream_fetch command + TransformStream reassembly — SSE/fetchEventSource zero-change dual-runtime -- **code-server** (npm.md): OS dependency table + install->URL->credentials 3-line closure + PATH troubleshooting -- **mihomo** (wiki.metacubex.one): systemd unit template (Restart=always, LimitNOFILE, ExecReload=kill -HUP, journalctl) -- **Diataxis** (diataxis.fr): tutorials/how-to/reference/explanation 4 quadrants — the industry-standard doc structure - ---- - -## Audit follow-up (T17-audit-fix) - -The original T17 marked P6 and P10 as checked but a deeper code-level diff audit (against the GRILL plan acceptance criteria no false positives) found three real defects that the original 4 vitest assertions did NOT cover: - -1. **platform_remove / subscription_remove / platform_update fetch path broken (high severity).** The SPA's invokeHttp fallback sent DELETE /api/v1/platforms with body {name:"X"} and PATCH with a full camelCase body. The headless axum reverse-proxy (proxy_to_resin) was a pure forwarder — it forwarded the body-bearing collection DELETE verbatim to Resin, which expects DELETE /api/v1/platforms/{id} (path-param, no body). Resin returned 404/405 and the SPA's mutate calls silently failed in headless mode. The Tauri mode worked because the Rust IPC commands (platform_remove, platform_update, subscription_remove) do a list+match name-to-id translation in Rust before forwarding — but the headless mode bypassed those commands entirely. -2. **patch-body key shape not equivalent (medium).** The SPA TS wrapper ipcPlatformUpdate sends camelCase IPC keys (allocationPolicy, regexFilters, ...) and the Tauri Rust command rewrites them to Resin's snake_case form. The headless path forwarded camelCase — Resin would have rejected the PATCH even with a working id. -3. **Vitest P10 false positive.** The 4 T17 dual-mode assertions only exercised platform_add (POST collection) + platform_list (GET collection) — the two trivially-correct routes. Zero coverage for the three broken DELETE/PATCH routes. The no-false-positives P10 criterion was a false claim on the mutation surface that actually mattered. - -**Fix (industrial pattern B — BFF/agent-side translation, per atomcode research).** The headless axum proxy now does the same list+match name-to-id resolution the Tauri Rust commands do, BEFORE forwarding, for the three known translation routes. The SPA frontend contract stays name-based in both modes; resolve+mutate are atomic in one process (no client-side TOCTOU). Industry prior art surfaced by atomcode: Kong request-transformer (transform before upstream), AWS API Gateway mapping templates, LiteLLM (admin uses UUID, alias resolved server-side), Vaultwarden (read by name, mutate by UUID). Pattern C (name-based upstream) was rejected because Resin v1.2.0 has no name endpoints and we don't fork the upstream Go binary. - -**Changes.** -- src-tauri/src/headless_main.rs: proxy_to_resin now aggregates the body (64 MiB cap) and a new translate_request dispatcher rewrites (method, path, body) into the path-param form when needed. New pure helpers items_arr, id_for_name, url_encode_segment, rewrite_patch_body_snake_case — extracted as pure functions so unit tests can lock the contract without spinning up reqwest. -- src/lib/ipc.test.ts: 3 new T17-audit vitest assertions pin the SPA-side contract (frontend continues to send the business name in the JSON body to the collection URL; the server-side BFF resolves name->id after this fetch). -- src-tauri/src/headless_main.rs cfg(test) module bff_translate_tests: 12 cargo unit tests covering items-wrapper vs bare-array resolution, empty-id rejection, URL encode behaviour, PATCH name stripping + camelCase to snake_case translation, null-field drop, enum/length validation, and empty/non-object body rejection. - -**Verification.** -- cargo test -p egressapikey-app --bin egressapikey-headless --features headless -> 12 passed. -- npx vitest run -> 255 passed (was 252; +3 audit assertions). -- npx tsc --noEmit clean. -- npm run i18n:check -> 363 keys / 18 locales match. -- cargo build --release -p egressapikey-app --bin egressapikey-headless --features headless -> 4.7MB exe staged at release/windows-backend/egressapikey-headless.exe. -- Smoke: launched the binary, process ALIVE, GET / -> 200, GET /api/v1/platforms (proxied to Resin sidecar) -> 200. - -**Audit trail preservation.** The original Phase 3 table (P6/P10 marked as done) is left intact above for the per-phase status snapshot; this section is the authoritative current truth. Following AGENTS.md section 6/10 directive: do NOT rewrite historical status rows into the new truth — leave them and add the follow-up. - -**Ponytail.** No new crates. The BFF layer is ~140 LoC added to headless_main.rs (a single match on three routes + a pure rewriter), and the existing axum/reqwest/serde_json/bytes deps already present in the headless Cargo.toml cover it. The frontend (src/lib/ipc.ts invokeHttp, CMD_TO_HTTP) is unchanged — the translation lives where the Rust IPC commands already live. diff --git a/docs/history/phases/GRILL_T18_CANVAS_PIPELINE_SYNC_PLAN.md b/docs/history/phases/GRILL_T18_CANVAS_PIPELINE_SYNC_PLAN.md deleted file mode 100644 index 74294ff3..00000000 --- a/docs/history/phases/GRILL_T18_CANVAS_PIPELINE_SYNC_PLAN.md +++ /dev/null @@ -1,148 +0,0 @@ -# GRILL T18 — Canvas + Pipeline Sync Plan - -> Canon: ADR-0042 (decisions) + CONTEXT.md deltas (terms) + this file (phases). -> Source grill rounds: R1 (Q1-Q7) + R2 (Q8-Q16) with atomcode research on Q8 / Q16. - -## Decisions locked - -| Q | Topic | Answer | -|----|-------|--------| -| Q1 | Port state sync source | A: health chip on EntryPortNode, data from port_health_check + PortMapping.enabled/auth_required | -| Q2 | Port→Platform binding source | A: whitebox egressapikey-ports.json is truth; Resin rebuilt from whitebox on restart | -| Q3 | A-strategy selected-nodes sync | A-bis: C-column stays global pool; edges filter per-platform (not per-platform C column) | -| Q4 | B-strategy parameter display | B: B badge shows strategy params (round-robin N, latency threshold) via i18n templates | -| Q5 | region viewMode existence | A: keep dual viewMode; region still useful for "how many nodes per region" mental model | -| Q6 | Config-file entry placement | A: FileCog moved to right-top toolbar with dropdown (ports.json / strategy.json); left-bottom FileCog removed | -| Q7 | Home button (reset center) | A: already exists (L636-638 setViewport({x:0,y:0,zoom:1})) | -| Q8 | Health chip data source | atomcode: Rust watch_port_health Channel + single ticker + batch probe + concurrency cap 10 + atomic reentry guard + TTL skip + exponential backoff (5 fails → Dead) + idle pause when tab hidden. Front-end single subscription + Map update + React.memo | -| Q9 | auth_required display on EntryPortNode | A: lock icon 🔒/🔓 + i18n tooltip, no extra text line | -| Q10 | Port enable/disable switch | A: PortForwarder controls listen (enabled=false → not listening), whitebox enabled field is truth, Resin unaware | -| Q11 | B-strategy param i18n keys | A: per-strategy i18n template with {{param}} interpolation; strategyConfig gains b_class_params field | -| Q12 | strategyConfig vs Resin sync | A: strategyConfig is single source of truth (ADR-0036); sync merge overwrites Resin values; Resin direct edits overwritten by next strategy_apply | -| Q13 | port_health_check protocol | A: already implemented (L476-481), pass port.protocol to probe | -| Q14 | MiniMap i18n | A: already i18n (L1009 ariaLabel), no change | -| Q15 | Resin restart rebuild from whitebox | A: new restore_ports_from_whitebox logic after boot_resin success | -| Q16 | Manual mode canvas interaction | atomcode: canvas drag stays group-level only (no leaf-node handles); manual selection via expanded card checkbox → manual_nodes → strategy_engine → region_filters PATCH | - -## Phase execution plan - -### Phase 1 — Port health chip + batch probe foundation - -**Files**: src-tauri/src/commands/mod.rs (new watch_port_health Channel command), src-tauri/src/forwarder.rs (batch probe logic), src/views/TopologyView.tsx (EntryPortNode chip + Channel subscription), src/lib/ipc.ts (TS wrapper for watch_port_health) - -**Work**: -1. Rust: new `watch_port_health(on_event: tauri::ipc::Channel<PortHealthSnapshot>)` command. Single Tokio task: ticker (interval = max(5s, k·ln(1+N)) where N = port count), batch probe with concurrency cap 10, atomic reentry guard (AtomicBool), TTL skip (skip if last probe < interval), exponential backoff for dead ports (interval × 2^min(fails,5), cap 5m), pause when tab hidden (shell emits pause/resume events). -2. Rust: PortHealthSnapshot { revision: u64, ports: Vec<{port: u16, alive: bool, fails: u32, latency_ms: Option<u32>}> } serialized as one Channel message per tick. -3. TS: new `ipcWatchPortHealth(callback)` wrapper using `@tauri-apps/api/event` Channel. TopologyCanvas subscribes on mount, unsubscribes on unmount. health state stored in a Map<u16, HealthState> in zustand topology store. EntryPortNode reads health from store. -4. EntryPortNode: 4-state visual encoding (alive=green dot, degraded=amber, dead=red + card grayed, restarting=blue pulse). Lock icon for auth_required. i18n: topology.portAlive / portDead / portDegraded / portRestarting / portAuthRequired / portAuthNotRequired. -5. Remove per-port ipcPortHealthCheck from sync() poll loop (if any). - -**Tests**: cargo test (batch probe logic + backoff math), vitest (health chip renders 4 states + lock icon). - -**Est**: ~250 lines Rust + ~80 lines TS. - -### Phase 2 — Port enable/disable switch - -**Files**: src/views/PlatformsView.tsx (toggle button), src/lib/ipc.ts (ipcPortToggle wrapper), src-tauri/src/commands/mod.rs (port_toggle command), src-tauri/src/forwarder.rs (listen/unlisten), src/views/TopologyView.tsx (EntryPortNode grayed + lock when disabled) - -**Work**: -1. Rust: new `port_toggle(port, enabled)` command. Updates whitebox egressapikey-ports.json enabled field (atomic write), calls PortForwarder to listen (enabled=true) or stop listening (enabled=false). Resin endpoint untouched. -2. TS: `ipcPortToggle(port, enabled)` wrapper. -3. PlatformsView: each port card top-right corner Toggle switch. Calls ipcPortToggle. Toast i18n platform.portEnabled / portDisabled. -4. TopologyView EntryPortNode: if PortMapping.enabled=false → card opacity-40 + lock icon + tooltip "已禁用" (i18n topology.portDisabled). Health probe skips disabled ports. - -**Tests**: cargo test (forwarder listen/unlisten on toggle), vitest (toggle calls ipcPortToggle + toast). - -**Est**: ~120 lines Rust + ~60 lines TS + i18n keys × 18. - -### Phase 3 — B-strategy parameter display + i18n - -**Files**: src/lib/strategy.ts (b_class_params type), crates/resin-core/src/strategy_engine.rs (PlatformStrategy gains b_class_params field), src/views/TopologyView.tsx (PlatformNode B badge with params), src/locales/*/topology.json (6 new i18n keys) - -**Work**: -1. Rust: PlatformStrategy gains `b_class_params: BClassParams` with optional fields: round_robin_n: Option<u32>, latency_threshold_ms: Option<u32>, quality_score: Option<u32>. serde default. -2. TS: StrategyId → i18n key mapping gains param interpolation. New i18n keys: - - topology.bClassRandom = "真随机" - - topology.bClassRoundRobin = "轮询 N={{n}}" - - topology.bClassLatency = "低延时 (<{{threshold}}ms)" - - topology.bClassQuality = "质量分>={{score}}" - - topology.bClassBandwidth = "带宽优先" - - topology.bClassProtocolWeight = "协议权重" -3. PlatformNode B badge: `B: {t(key, params)}` where params come from strategyConfig.b_class_params. -4. PlatformsView strategy panel: per-strategy param input (round-robin N stepper, latency threshold slider, quality score input). - -**Tests**: cargo test (b_class_params serde round-trip), vitest (B badge renders params with interpolation). - -**Est**: ~100 lines Rust + ~60 lines TS + 6 i18n keys × 18 locales. - -### Phase 4 — Manual mode card checkbox + canvas drag guard - -**Files**: src/views/TopologyView.tsx (RegionGroupNode / SubscriptionGroupNode expanded node row checkbox, onConnect isValidConnection guard), src/lib/ipc.ts (ipcStrategyConfigPut with manual_nodes update), src-tauri/src/commands/mod.rs (strategy_config_put accepts manual_nodes) - -**Work**: -1. RegionGroupNode / SubscriptionGroupNode: when expanded, each node row gains a checkbox. Checkbox state = whether strategyConfig.platforms[?].manual_nodes includes row.node_hash. Toggle → update strategyConfig → strategy_apply. -2. onConnect guard: if target starts with "leaf-" or is a non-group node → reject (return early, no PATCH). Only platform→subgroup / platform→regiongroup / entry→platform allowed. -3. A-class badge on PlatformNode: if a_class=manual, badge shows "手动(N节点)" i18n topology.aClassManualCount with {{n}} = manual_nodes.len(). -4. strategy_apply: if a_class=manual, strategy_engine maps manual_nodes → regions → PATCH Resin region_filters. If a_class=region, direct region_filters PATCH. If a_class=subscription, map subscription names → regions → PATCH. - -**Tests**: vitest (checkbox toggles manual_nodes + strategy_apply called), cargo test (strategy_engine manual_nodes → regions mapping). - -**Est**: ~150 lines TS + ~30 lines Rust. - -### Phase 5 — Config-file entry toolbar reorganization - -**Files**: src/views/TopologyView.tsx (CanvasControls: remove FileCog from left-bottom, add config dropdown to right-top toolbar), src/locales/*/topology.json (topology.openPortsConfig + topology.openStrategyConfig i18n keys) - -**Work**: -1. Remove FileCog button from left-bottom CanvasControls (L642-645). -2. Add right-top floating toolbar (absolute top-2 right-2 z-10) with a FileCog button + dropdown menu: - - "打开端口配置 (egressapikey-ports.json)" → opens file in OS default editor - - "打开策略配置 (egressapikey-strategy.json)" → opens file in OS default editor -3. Both use ipcGetConfigDir + openPath. i18n: topology.openPortsConfig / topology.openStrategyConfig. - -**Tests**: vitest (dropdown renders two options + click triggers openPath). - -**Est**: ~40 lines TS + 2 i18n keys × 18. - -### Phase 6 — Resin restart port restore from whitebox - -**Files**: src-tauri/src/main.rs (post boot_resin call), src-tauri/src/commands/mod.rs (restore_ports_from_whitebox), crates/resin-core/src/whitebox_config.rs (read entry_ports + create endpoints helper) - -**Work**: -1. After boot_resin success in main.rs setup(), call restore_ports_from_whitebox(whitebox, sidecar). -2. restore_ports: for each PortMapping in whitebox.entry_ports: if enabled → POST /api/v1/endpoints {port, protocol, platform_name, account, auth_required} to Resin. If Resin returns 409 (already exists) → skip. Log tracing::info! per port. -3. This closes the Q2=A loop: whitebox is truth source, Resin rebuilds from it on restart. - -**Tests**: cargo test (restore_ports_from_whitebox mockito: POST endpoint for enabled port, skip for disabled, skip on 409), integration smoke (restart Resin → ports restored). - -**Est**: ~80 lines Rust. - -### Phase 7 — Full gate: build + i18n + test + exe smoke - -**Files**: N/A (verification only) - -**Work**: -1. `pnpm i18n:check` — all new keys present in 18 locales. -2. `pnpm test` — all new vitest green. -3. `cargo test -p resin-core --lib` — all new cargo green. -4. `pnpm exec tsc --noEmit` — clean. -5. `pnpm build` → new Vite chunk hash. -6. `cargo build --release -p egressapikey-app --features custom-protocol` → exe. -7. Stage exe + resin.exe sidecar to release/windows-gui/. -8. Grep Vite chunk hash in exe bytes (stale-bundle guard). -9. Smoke launch: MainWindowTitle="EgressAPIKEY", WorkingSet ~36MB, resin.exe child alive, /healthz 200. -10. `codegraph sync .` + `git push`. - -**Acceptance**: all gates green, exe fresh, smoke alive. - -## Test closed-loop summary - -| Phase | Cargo tests | Vitest tests | Smoke | -|-------|-------------|--------------|-------| -| 1 | batch probe + backoff math | 4-state chip + lock icon | — | -| 2 | forwarder listen/unlisten | toggle + toast | — | -| 3 | b_class_params serde | B badge params interpolation | — | -| 4 | manual_nodes → regions | checkbox + strategy_apply | — | -| 5 | — | dropdown + openPath | — | -| 6 | restore_ports mockito | — | restart → ports restored | -| 7 | all green | all green | exe alive + resin healthy | diff --git a/docs/history/phases/GRILL_T19_NODE_POOL_FIX_PLAN.md b/docs/history/phases/GRILL_T19_NODE_POOL_FIX_PLAN.md deleted file mode 100644 index 3262d175..00000000 --- a/docs/history/phases/GRILL_T19_NODE_POOL_FIX_PLAN.md +++ /dev/null @@ -1,144 +0,0 @@ -# GRILL T19 — Node Pool Page Fix Plan - -> Canon: ADR-0044 (decisions) + CONTEXT.md deltas (4 terms) + this file (5-phase execution). -> Source grill rounds: R1 (Q1-Q4) with 3 atomcode research passes (node-pool-UX / resin-sub-scheduler / probe-params). -> Authority: ADR-0036 strategy-single-source-truth still holds; this plan layers per-node probe + local-subscription refresh on top, no strategy pipeline change. - -## Decisions locked - -| Q | Topic | Answer | -|---|-------|--------| -| Q1 | Default collapse + hide unhealthy + latency sort + delay> syntax | A — collapse Set seeded after first refresh, hide-unhealthy toggle reusing `isHealthy`, 3-state latency sort reusing `reference_latency_ms`, search supports `delay>100` / `delay=timeout` filters | -| Q2 | Why node pool never updates | A — Path 丙: shell re-fetches remote Clash YAML (UA rotate) + `clash_yaml_to_proxies_block` convert + PATCH subscription content; new `subscription_refresh(name)` IPC + per-group refresh button | -| Q3 | Per-node ping like clash-verge | A — wire to Resin native per-node endpoint: `POST /nodes/{hash}/actions/probe-egress` (egress IP + EWMA latency) and `/actions/probe-latency` (sync). New `ResinClient::probe_node_egress` + `probe_node_latency` + `node_probe(node_hash, kind)` IPC + per-card test button | -| Q4 | Whitebox probe params + default behavior | 甲+丙+default group concurrency probe, params whitebox editable. Default = manual single-probe (甲), per-group batch test button (丙), concurrency=10 / timeout=10s / test_url=`https://www.gstatic.com/generate_204` / batch_on_load=false. All four knobs persisted in `settings.json#nodeProbe` + surfaced in Settings page. Resin hot-update knobs (`max_consecutive_failures`, `max_latency_test_interval`, `max_egress_test_interval`, `latency_test_url`, `latency_authorities`, `p2c_latency_window`, `latency_decay_window`) surfaced via `system_config_get/patch` already in ResinClient. Parameter sources: atomcode research 3 (Resin ProbeManager defaults + clash-verge delayManager). | - -## Parameter defaults (sources: atomcode 3) - -| Knob | Default | Source | Whitebox surface | -|------|---------|--------|------------------| -| `nodeProbe.concurrency` | 10 | clash-verge-rev hard cap `min(concurrency, n, 10)` | settings.json + Settings panel | -| `nodeProbe.timeout_ms` | 10000 | clash-verge `DEFAULT_DELAY_TIMEOUT = 10000`; shell takes min(10s, Resin RESIN_PROBE_TIMEOUT=15s) | -| `nodeProbe.test_url` | `https://www.gstatic.com/generate_204` | Resin default `latency_test_url` (hot-update via `/system/config`) | -| `nodeProbe.batch_on_load` | false | clash-verge #979 — full auto-ping is community pain point; 甲 = manual by default | -| Resin `max_consecutive_failures` | 3 | Resin default | Settings panel → `/system/config` PATCH | -| Resin `max_latency_test_interval` | 1h | Resin default | Settings panel → `/system/config` PATCH | -| Resin `max_egress_test_interval` | 24h | Resin default | Settings panel → `/system/config` PATCH | -| Resin `latency_authorities` | [gstatic.com, google.com, cloudflare.com, github.com] | Resin default | Settings panel → `/system/config` PATCH | - -## Phase execution plan - -### Phase 1 — Default collapse + hide-unhealthy + latency sort + delay> filter - -**Files**: `src/views/NodesView.tsx` (L84 collapsed Set, L112 filtered useMemo, new sort state, new `hideUnhealthy` toggle, new `parseDelayQuery` helper) - -**Work**: -1. `collapsed` Set seeded with every `sub name` after first `refresh()` returns (first paint = all collapsed). Current `new Set()` = all expanded → flip to seed-all. -2. New `hideUnhealthy` boolean state, default false, toggle chip in header. When true, `filtered` excludes nodes where `!isHealthy(n)`. -3. New `sortMode` 3-state: 'default' → 'latency-asc' → 'latency-desc' → cycle. Header sort chip. `filtered` sorts by `reference_latency_ms` (null = Infinity timeout-first in asc, -Infinity in desc). -4. New `parseDelayQuery(q: string)` pure helper. Recognizes `delay>N`, `delay<N`, `delay=timeout`, `delay=error` (case-insensitive). Returns `{ text: string, delayFilter?: (ms) => boolean }`. `filtered` combines text + delay filter. -5. `search` state input + 2 new header chips (hide-unhealthy 🩺, sort ↑/↓/default) + collapse-all/expand-all buttons already exist. - -**i18n keys** (18 locales × 6 keys = 108 new entries): -- `nodes.hideUnhealthy` (en: "Hide unhealthy", zh: "隐藏无效节点") -- `nodes.sortDefault` (en: "Sort: default", zh: "排序:默认") -- `nodes.sortLatencyAsc` (en: "Sort: latency ↑", zh: "排序:延迟↑") -- `nodes.sortLatencyDesc` (en: "Sort: latency ↓", zh: "排序:延迟↓") -- `nodes.delayFilterHint` (en: "Use delay>100 or delay=timeout", zh: "可用 delay>100 或 delay=timeout 筛选") -- `nodes.collapseAllDefault` (en: "Groups are collapsed by default", zh: "订阅分组默认折叠") - -**Loop test** (vitest, `src/views/NodesView.test.tsx` +5 assertions): -- `parseDelayQuery` pure helper: `delay>100` admits 200, rejects 50; `delay=timeout` admits 9999/null, rejects 200; plain text passes through -- `hideUnhealthy` toggle excludes `failure_count>0` nodes from `filtered` -- 3-state `sortMode` cycle produces ascending / descending order on `reference_latency_ms` -- `collapsed` Set seeded with all sub names after first refresh (integration test against mock refresh) -- `delay=error` filter matches nodes with missing `reference_latency_ms` - -**Estimate**: ~110 lines TS, 0 Rust, 0 IPC, 6 i18n keys ×18 locales. - -### Phase 2 — Subscription refresh IPC (local re-fetch + PATCH content) - -**Files**: `crates/resin-core/src/resin_client.rs` (new `refresh_subscription_content(id, content)` → PATCH `/api/v1/subscriptions/{id}`), `src-tauri/src/commands/mod.rs` (new `subscription_refresh(name)` command: list → match name → id → fetch_clash_subscription(url) → clash_yaml_to_proxies_block → PATCH), `src/lib/ipc.ts` (`ipcSubscriptionRefresh(name)`), `src/views/NodesView.tsx` (per-group refresh button on sub title row) - -**Work**: -1. Rust `ResinClient::refresh_subscription_content(id, content)` — PATCH body `{content, source_type:"local"}` to `/api/v1/subscriptions/{id}`. Reuses existing bearer+loopback guards. 1 new mockito test. -2. Rust `subscription_refresh(name: String, app)` command: `list_subscriptions()` → match by name → fetch `subscription.url` (already stored by earlier `subscription_add`? verify shape) via existing `fetch_clash_subscription` (clash UA rotate) → `clash_yaml_to_proxies_block` → `refresh_subscription_content(id, block)`. Every step `tracing::info!`/`warn!`. 1 new cargo unit test. -3. TS `ipcSubscriptionRefresh(name)` wrapper, validates name length+control chars. `NodesView.tsx` group title row gains a "🔄" button (i18n `nodes.refreshSub`) that calls the wrapper and triggers `refresh()` poll to re-read. -4. If subscription has no stored url (legacy): toast `nodes.refreshNoUrl` + no-ops. - -**i18n keys** (18 × 3 = 54 new): `nodes.refreshSub`, `nodes.refreshingSub`, `nodes.refreshNoUrl`. - -**Loop test**: 1 new cargo (mockito happy PATCH), 2 vitest (refresh button calls wrapper with name; `assertShortName` guard fires on empty). Existing `ipc.test.ts` pattern. - -**Estimate**: ~50 Rust + ~25 TS, 3 i18n keys ×18. - -### Phase 3 — Per-node probe IPC (Resin native endpoint) - -**Files**: `crates/resin-core/src/resin_client.rs` (new `probe_node_egress(node_hash)` + `probe_node_latency(node_hash)`), `src-tauri/src/commands/mod.rs` (new `node_probe(node_hash, kind)`), `src/lib/ipc.ts` (`ipcNodeProbe(hash, kind)`), `src/views/NodesView.tsx` (per-card test button → local latency + egress_ip update) - -**Work**: -1. Rust `ResinClient::probe_node_egress(hash)` — POST `/api/v1/nodes/{hash}/actions/probe-egress` (returns `{egress_ip, latency_ms_ewma}`). `probe_node_latency(hash)` — POST `/actions/probe-latency` (sync, returns `{latency_ms}`). Both reuse bearer+loopback. 2 new mockito tests. -2. Rust `node_probe(node_hash: String, kind: String, app)` — validate hash length ≤128 + hex/control reject; validate `kind` ∈ `{"egress","latency"}`; forward. Cancellation token from `SidecarHandle` (same as health probe). 1 new cargo unit test. -3. TS `ipcNodeProbe(hash, kind)` wrapper, 2 new vitest (forward + reject invalid kind). -4. `NodesView.tsx` per-card "📡" button on hover row. Calls `ipcNodeProbe(hash, 'latency')` first (quick), updates the row's `reference_latency_ms` + color; optional "出口 IP" button → `ipcNodeProbe(hash, 'egress')` fills `egress_ip`. In-flight = `testing` state (spinner). Error toast: `nodes.probeError`. - -**i18n keys** (18 × 4 = 72): `nodes.probeLatency`, `nodes.probeEgress`, `nodes.probing`, `nodes.probeError`. - -**Loop test**: 2 cargo mockito + 2 vitest + 1 component test (button click → wrapper called → state updates). - -**Estimate**: ~60 Rust + ~40 TS, 4 i18n keys ×18. - -### Phase 4 — Per-group batch probe + whitebox config knobs - -**Files**: `src/lib/settings.ts` (new `loadNodeProbe` + `saveNodeProbe`), `src/views/NodesView.tsx` (per-group "批量测速" button + concurrency gate), `src/views/SettingsView.tsx` (new "节点探测参数" panel exposing `nodeProbe` + Resin `/system/config` knobs), `src/lib/ipc.ts` (no new wrapper, uses `ipcNodeProbe` + existing `system_config_get/patch` if exposed — verify) - -**Work**: -1. `settings.ts` new `loadNodeProbe()` / `saveNodeProbe(cfg)` reading/writing `settings.json#nodeProbe` with defaults: `{concurrency:10, timeout_ms:10000, test_url:"https://www.gstatic.com/generate_204", batch_on_load:false}`. Config struct in `src/lib/types.ts`. -2. `NodesView.tsx` per-group "批量测速" button (i18n `nodes.batchProbe`). On click: read `loadNodeProbe()`, gate concurrent inflight probes to `concurrency` cap, timeout each at `timeout_ms` (AbortController), results update each row's `reference_latency_ms`. Skip if `batch_on_load === false` on initial load (甲 default). 3 new vitest: concurrency cap respected, timeout fires, batch_on_load=false skips auto-batch. -3. `SettingsView.tsx` new "节点探测参数" panel card. 4 inputs: concurrency (number 1-50), timeout_ms (number 1000-30000), test_url (string/url), batch_on_load (toggle). Save button writes via `saveNodeProbe()`. Also a "Resin 探测参数" sub-card exposing the 7 hot-update knobs (`max_consecutive_failures`, `max_latency_test_interval`, `max_authority_latency_test_interval`, `max_egress_test_interval`, `latency_test_url`, `latency_authorities`, `p2c_latency_window`, `latency_decay_window`) via `system_config_get` (load on mount) + `system_config_patch` (save). Check existing `ipcSystemConfigGet`/`ipcSystemConfigPatch` wrappers — if missing, add in this phase. i18n: `settings.nodeProbe` namespace. -4. Validate: concurrency 1-50, timeout_ms 1000-30000 at both TS + Rust boundary (if any). `test_url` must `http(s)://` prefix. - -**i18n keys** (18 × 10 = 180): `nodes.batchProbe`, `nodes.batchProbing`, `settings.nodeProbeTitle`, `settings.nodeProbeConcurrency`, `settings.nodeProbeTimeout`, `settings.nodeProbeTestUrl`, `settings.nodeProbeBatchOnLoad`, `settings.resinProbeTitle`, `settings.resinMaxFailures`, `settings.resinLatencyInterval`. - -**Loop test**: 3 vitest + 1 component (Settings panel round-trip load → edit → save → reload). - -**Estimate**: ~80 TS, 0 Rust (barring `system_config` wrapper gap), 10 i18n keys ×18. - -### Phase 5 — Full gate build + test + exe smoke + AGENTS.md + i18n:check - -**Files**: `AGENTS.md` (new §31 "P25-T19 node-pool fixes"), `docs/CONTEXT.md` (4 new terms), i18n:check gate - -**Work**: -1. `pnpm i18n:check` = 363+ (6+3+4+10) = 386 keys / 18 locales green. -2. `pnpm exec tsc --noEmit` green. -3. `pnpm test` = baseline + P1 (5) + P2 (2) + P3 (4) + P4 (4) = 15 new vitest. -4. `cargo test -p resin-core --lib` = baseline + P2 (1) + P3 (3) = 4 new cargo. -5. `pnpm build` (tsc + vite) green, new chunk hash. -6. `cargo build --release -p egressapikey-app --features custom-protocol` → exe staged at `release/windows-gui/EgressAPIKEY.exe` (host triple `x86_64-pc-windows-msvc`). -7. Grep new Vite chunk hash in staged exe bytes (hard close-loop, AGENTS §5). -8. Smoke launch: `MainWindowTitle='EgressAPIKEY'`, resin.exe child alive, /healthz 200. -9. `codegraph sync .` re-index. -10. `git add` + commit message in `.codex-tmp/commit-msg.txt` + `git commit -F` + `git push`. -11. AGENTS.md §31 documents: default-collapse, hide-unhealthy, delay> syntax, subscription_refresh, per-node probe endpoint, batch probe gate, nodeProbe config knob, Resin `/system/config` plumbing, test counts, i18n key count. - -**CONTEXT.md deltas** (4 new terms): -- `defaultCollapsed` — NodesView groups collapse by default after first refresh (seed-all `collapsed` Set). Industry mental model: metacubexd `hideUnAvailableProxies`, clash-verge #979. -- `subscriptionRefresh` — shell-initiated local subscription content refresh. Shell re-fetches remote Clash YAML (UA rotate), converts via `clash_yaml_to_proxies_block`, PATCHes `/api/v1/subscriptions/{id}` content. Resin `/actions/refresh` on local source is a no-op re-parse, so this path is necessary for real remote updates. -- `nodeProbe` — manual or batch per-node ping via Resin `/actions/probe-egress` or `/actions/probe-latency`. Whitebox settings.json knobs: `concurrency`=10, `timeout_ms`=10000, `test_url`=gstatic generate_204, `batch_on_load`=false. Sources: clash-verge `DEFAULT_DELAY_TIMEOUT` + Resin `latency_test_url`. -- `probeKind` — enum `{"egress","latency"}`. `egress` returns exit IP + EWMA latency (heavier, 24h internal cap); `latency` is sync quick ping. Both go through the same `node_probe` IPC, dispatched by `kind`. - -**Estimate**: ~30 docs, 4 CONTEXT terms, gate verification. - -## ADR-0044 (decisions locked) - -1. **S1 — Default collapse + hide-unhealthy + latency sort**: `collapsed` Set seeded after first refresh. Industry has no default-collapse but `hideUnAvailable` is the most-requested missing feature; we combine the two. 3-state sort reuses `reference_latency_ms`. -2. **S2 — Local subscription refresh via shell re-fetch**: local source_type means Resin re-parses in-memory content; to actually pull fresh remote nodes the shell must re-fetch + re-POST content. Rejected: auto-poll subscriptions from shell (Resin `update_interval` already exists for remote source_type — but our shell uses local so this path does not apply). Rejected: fork Resin to add a local-refresh remote hook (A4-1 style; overkill for this phase). -3. **S3 — Per-node probe via Resin native endpoint**: `probe-egress` and `probe-latency` exist in Resin v1.2.0 (`ProbeEgressSync`/`ProbeLatencySync`). Shell just wires the endpoint through; no probe logic in the shell. Rejected: shell-side mihomo `/proxies/{name}/delay` (we have no mihomo API surface for nodes-by-hash; Resin owns node identity). -4. **S4 — Whitebox probe knobs + default 甲+丙 with group concurrency**: defaults from clash-verge + Resin industry research (`concurrency=10`, `timeout=10s`, `test_url=gstatic`, `batch_on_load=false`). Resin hot-knobs surfaced separately via `/system/config`. Rejected: auto-batch on first load (clash-verge #979 community pain point); manual is the polite default. - -## Rejected alternatives (recorded for the audit trail) - -- Virtual scrolling for node cards: no industry precedent (clash-verge/metacubexd/v2rayN all use group collapse + filter, not virtual rows); we have `VIRTUAL_THRESHOLD=50` already. -- Auto-batch probe on load: clash-verge #979 community rejects; passive 默认 manual. -- Shell-side TCP ping tool: would bypass Resin's health state; Resin `probe_node_egress` returns Resin's own EWMA so the shell stays consistent with routing decisions. -- Fork Resin Go to add time/url/env-var knobs: `RESIN_PROBE_CONCURRENCY` and `RESIN_PROBE_TIMEOUT` are EnvConfig (not hot-update); `/system/config` knobs (max_*, latency_*) ARE hot-update so we surface those instead and leave env-only knobs to startup. diff --git a/docs/history/phases/GRILL_T20_IPC_ERROR_CONTRACT.md b/docs/history/phases/GRILL_T20_IPC_ERROR_CONTRACT.md deleted file mode 100644 index 3634215f..00000000 --- a/docs/history/phases/GRILL_T20_IPC_ERROR_CONTRACT.md +++ /dev/null @@ -1,90 +0,0 @@ -# GRILL_T20_IPC_ERROR_CONTRACT.md — IPC Error Contract Hardening - -> Grill T20: 5-question decision chain. Q1-Q5 all answered. Plan finalized. - -## Decisions - -| Q | Decision | Description | -|---|----------|-------------| -| Q1 | A — Full refactor | All 64 `#[tauri::command]` return `Result<T, IpcError>`. Residual 3 `Result<_, String>` (L172/L1523/L1934) + 1 helper (L1968) to convert. | -| Q2 | A — Preventive lint + fault-tolerant | (b) fault-tolerant already done: `translateError` in `i18n-error.ts` handles Error vs IpcErr vs string. (a) ESLint custom rule `no-raw-error-in-toast` to prevent future regression. | -| Q3 | A — Toast action button full variant | `showToast` extends to 3rd param `action: { label, onClick }`. BindConflict → "换个端口" button calls `ipcPortSuggest()`. InvalidStrategy → "查看策略文档" link. ResinUpstream → "重试" button. Internal → no action. | -| Q4 | A — Full translateError + lint guard | **Finding: all 13 "raw catches" already use translateError** — SettingsView 7/7, NodesView 2/3 (1 is console.warn), DiagnosticsView 3/3 (zeroed-result pattern, not user-facing). Actual work = ESLint lint rule only. | -| Q5 | A — Full test coverage | cargo: `map_resin_error` 13+ branch tests + `extract_port_from_residual` boundary tests + IpcError 4 variant serde round-trip. vitest: view-level translateError closed-loop tests. ~14 cargo + ~6 vitest = 20 tests. | - -## Execution Plan (5 phases) - -### Phase 1 — Residual String → IpcError (Q1) -**Scope**: convert 3 `#[tauri::command]` + 1 helper from `Result<_, String>` to `Result<_, IpcError>`. -- L172: `port_upsert` (already has IpcError on 2021 variant but L172 is a different signature — verify) -- L1523: `strategy_apply` or similar command -- L1934: another command -- L1968: `validate_port_segments` helper -**IpcError enum** (already defined in resin-core): BindConflict / InvalidStrategy / ResinUpstream / Internal, each with `i18n_key` field. serde externally-tagged. -**map_resin_error** already handles 13+ Resin error patterns. -**Actions**: read each residual site, convert String → IpcError using `IpcError::internal("error.xxx")` or specific variant. -**Verification**: `cargo build -p egressapikey-app --features custom-protocol` green. `cargo test -p resin-core --lib` green. -**Estimated**: ~40 lines mechanical. - -### Phase 2 — ESLint no-raw-error-in-toast rule (Q2a + Q4) -**Scope**: custom ESLint rule that flags any `catch (e)` block where `e` is passed to a function that is NOT `translateError` (i.e. raw `String(e)`, `e.message`, or bare `e` in a JSX expression). -**File**: `eslint.config.cjs` (exists, currently `rules: {}`). -**Rule**: custom processor scans catch blocks in .tsx/.ts for: - - `showToast("err", e)` or `showToast("err", String(e))` → error - - `showToast("err", e.message)` → error - - Any `setXxx(e)` or `setXxx(String(e))` where the setter is not `setXxx(translateError(e, t))` → warn -**Allow**: `console.warn(e)` / `console.log(e)` (debug not user-facing). -**vitest AST guard**: a test that parses all .tsx/.ts files and asserts no catch block passes raw `e` to a user-facing display function. -**Estimated**: ~30 lines rule + ~20 lines vitest guard = ~50 lines. - -### Phase 3 — Toast action button (Q3) -**Scope**: extend `showToast` signature from `(kind, msg)` to `(kind, msg, action?)` where `action: { label: string, onClick: () => void }`. -**PlatformsView.tsx L205**: modify `showToast` definition. -**BindConflict path**: on port-bind error, toast shows "换个端口" button → `ipcPortSuggest()` → fill new port in form. -**InvalidStrategy path**: toast shows "查看策略文档" → link to docs/STRATEGY.md (or tooltip). -**ResinUpstream path**: toast shows "重试" → re-invoke last failed IPC. -**Internal path**: no action button. -**i18n keys**: `error.action.changePort`, `error.action.viewDocs`, `error.action.retry` across 18 locales. -**Estimated**: ~60 lines TSX + ~10 lines i18n = ~70 lines. - -### Phase 4 — Full test coverage (Q5) -**Rust cargo tests** (`src-tauri/src/commands/mod.rs` `#[cfg(test)]` module): - - `map_resin_error` 13+ branch tests: cannot_delete_default, auth_required, auth_failed, url_parse, invalid_protocol, upstream_connect, upstream_request, bind_conflict_with_port, bind_conflict_no_port, conflict, not_found, bad_request, unauthorized, subscription_fetch_525, subscription_fetch_403, unknown_error_passes_through - - `extract_port_from_residual` boundary tests: valid_port_in_message, no_port_returns_none, multiple_numbers_picks_first_valid, port_at_boundary (0, 65535, 65536) - - `IpcError` 4 variant serde round-trip: BindConflict { port, i18n_key }, InvalidStrategy { value, accepted, i18n_key }, ResinUpstream { status, excerpt, i18n_key }, Internal { msg, i18n_key } — serialize → deserialize → assert field equality -**vitest tests**: - - SettingsView: 1 test asserting `setConfigMsg(translateError(e, t))` is called in catch - - DiagnosticsView: 1 test asserting zeroed-result pattern (not user-facing error) - - NodesView: 1 test asserting `setError(translateError(e, t))` in catch - - PlatformsView: 1 test asserting toast action button renders for BindConflict -**Estimated**: ~200 lines test code. - -### Phase 5 — Build + smoke + commit -- `pnpm build` → new chunk hash -- `cargo build --release -p egressapikey-app --features custom-protocol` → ~5min -- Chunk hash verification in exe bytes -- Stage to `release/windows-gui/` -- Smoke: MainWindowTitle + sidecar boot + port operations -- `pnpm test` all green -- `cargo test -p resin-core --lib` all green -- `pnpm i18n:check` all 18 locales match -- `pnpm exec tsc --noEmit` green -- Commit + push - -## Acceptance Criteria - -1. **Zero residual `Result<_, String>` in `#[tauri::command]` signatures** (Q1) -2. **ESLint `no-raw-error-in-toast` rule active** — `pnpm exec eslint src/` passes; any new catch with raw `e` in toast/display triggers lint error (Q2a/Q4) -3. **Toast action buttons wired** for BindConflict (change port), InvalidStrategy (view docs), ResinUpstream (retry) (Q3) -4. **`map_resin_error` has >=13 cargo branch tests** + **`extract_port_from_residual` has >=4 boundary tests** + **IpcError 4 variant serde round-trip has 4 cargo tests** (Q5) -5. **>=3 vitest view-level tests** asserting translateError in catch blocks (Q5) -6. **Build + smoke + chunk hash verified** (Phase 5) -7. **Zero `[object Object]` possible** in any user-facing error path (Q2b already done, lint guards future) - -## ADR -See `docs/adr/0045-ipc-error-contract-hardening.md`. - -## Industry references -- Tauri v2 IPC error pattern: `tauri::ipc::InvokeError` serializes `Result<T, E: Serialize>` as externally-tagged enum -- ESLint custom rule pattern: `@typescript-eslint/utils` AST visitor on `CatchClause` body -- Toast action button: shadcn/ui `sonner` toast pattern (action prop), clash-verge-rev error toast with retry button diff --git a/docs/history/phases/GRILL_T20_SUBSCRIPTION_REFRESH_NATIVE_PLAN.md b/docs/history/phases/GRILL_T20_SUBSCRIPTION_REFRESH_NATIVE_PLAN.md deleted file mode 100644 index 1f1f69e3..00000000 --- a/docs/history/phases/GRILL_T20_SUBSCRIPTION_REFRESH_NATIVE_PLAN.md +++ /dev/null @@ -1,210 +0,0 @@ -# GRILL T20 — Subscription Refresh Sink to Resin Native /actions/refresh - -Branch: T20 (parallel to T17/T19 audit) -Canon ADR: docs/adr/0047-subscription-refresh-native-resin-actions.md (ACCEPTED) -Supersedes: ADR-0044 S2 (refresh path only; S1/S3/S4 untouched) -Date: 2026-08-18 - -## Decision (grill Q1-Q3 all locked) - -- **Q1 = A**: Subscription refresh sinks entirely to Resin native POST /api/v1/subscriptions/{id}/actions/refresh. Delete the shell fetch+convert+patch chain (P13 B4 debt: fetch_clash_subscription + clash_yaml_to_proxies_block + refresh_subscription_content + 3 mockito tests, ~200 LoC). -- **Q2 = A-1**: Refresh real semantic = re-pull remote url. Resin v1.2.0 internal/topology/subscription_scheduler.go: if SourceTypeLocal -> body = in-memory content (no HTTP, no-op re-parse); else -> body = Fetcher(url). So source_type MUST be remote for refresh to actually pull new nodes. -- **Q3 = A-1-完整**: Both subscription_add AND subscription_refresh migrate to remote. A half-change would leave local where /actions/refresh is a no-op. Coupled edit, single commit. - -### Source-level evidence (Resin v1.2.0, indexed in ctx) - -- internal/service/control_plane_subscription.go RefreshSubscription(id) -> sync Scheduler.UpdateSubscription(sub). No source_type branch in the service layer; the branch is in the scheduler. -- internal/topology/subscription_scheduler.go UpdateSubscription: if SourceTypeLocal -> body = content; else -> body, err = s.Fetcher(attemptURL). Local = no-op re-parse; remote = HTTP fetch. -- cmd/resin/main.go const downloadUserAgent = "clash.meta". Resin already ships a clash-family UA. ADR-0044 S2 premise 1 ("Resin uses default UA") is FALSE. -- internal/netutil/downloader.go NewDirectDownloader(timeoutFn, userAgentFn) injects UA via callback. Premise 2 (flow-style choke) is UNVERIFIED in v1.2.0 — the shell-side convert was a defensive workaround that may be unnecessary, but it is deleted regardless because the shell no longer fetches. - -### Rejected alternatives (full list in ADR-0047) - -1. Keep shell fetch + just add /actions/refresh for local — dead end: /actions/refresh on local = no-op. -2. Shell fetch + POST remote — half-measure: duplicates Resin's own clash.meta fetcher for zero gain. -3. Shell fetch + convert + PATCH content (ADR-0044 S2) — what we are replacing; two wrong premises. -4. Fork Resin to add source_type-agnostic refresh — cosmetic fork, rejected per Ponytail. -5. Do nothing, document the local no-op — violates Q3 user expectation that refresh actually pulls. - ---- - -## Phase P1 — ResinClient: add refresh_subscription_native + delete 3 helpers - -File: crates/resin-core/src/resin_client.rs - -Add: -- pub async fn refresh_subscription_native(&self, id: &str) -> Result<Value> - -> POST {api_base}/api/v1/subscriptions/{id}/actions/refresh (bearer auth, no body) - -> on 2xx return serde_json::Value (empty object ok) - -> on non-2xx error with upstream status + 256B excerpt (same shape as other methods) - -> loopback guard inherited from &self (already constructed from SidecarHandle) - -Delete (P13 B4 debt, superseded by ADR-0047): -- pub async fn refresh_subscription_content (L283) -- pub async fn fetch_clash_subscription (L415) -- pub fn clash_yaml_to_proxies_block (L480) -- split_flow_fields helper (private, used only by clash_yaml_to_proxies_block) -- 3 mockito tests: mockito_refresh_subscription_content_patches_local_content (L1155) -- 3 cargo tests: clash_yaml_to_proxies_block_extracts_proxies_and_converts_flow_style (L782), clash_yaml_to_proxies_block_handles_unicode_names_and_single_quote_escape (L808), clash_yaml_to_proxies_block_rejects_yaml_without_proxies_key (L818), split_flow_fields_handles_quoted_comma_inside_value (if present) - -Add 1 new mockito test: -- mockito_refresh_subscription_native_posts_actions_refresh: mock POST /api/v1/subscriptions/{id}/actions/refresh returns 200 {} -> Ok(Value::Object). Assert the POST path + that no body is sent (Content-Length: 0 or absent). - -Closed loop verify: -- cargo test -p resin-core --lib baseline 137 -> expect 138 (delete ~4 tests, add 1, keep others) - ---- - -## Phase P2 — subscription_refresh IPC rewrite (commands/mod.rs) - -File: src-tauri/src/commands/mod.rs - -Current state (L649): - pub async fn subscription_refresh(name, url, admin_token hint? no — name only) - body: fetch_clash_subscription(url) -> clash_yaml_to_proxies_block -> client.refresh_subscription_content(id, block) (P13 B4 path) - -Rewrite to: - pub async fn subscription_refresh(app, name) -> Result<(), IpcError> - validate name (1..128 chars, no control, AGENTS s7.5) - lock State<SidecarHandle>, build ResinClient - client.list_subscriptions() - items_arr -> find item where item.name == name -> extract id - if not found -> Err(IpcError::Internal) with "subscription not found" - client.refresh_subscription_native(id).await - tracing::info!(subscription=%name, id=%id, "subscription refresh sink to /actions/refresh") - Ok(()) - -Delete: the fetch+convert+patch block; re-use subscription_id_for_name helper (already exists, items_arr shape). - -Add 2 cargo pure-helper tests: -- subscription_refresh_name_to_id_round_trip: given a list JSON with items wrapper + matching name -> returns Ok(id) -- subscription_refresh_unknown_name_returns_err: given a list with no matching name -> returns Err - -Delete the url parameter from the IPC signature (no longer needed; the refresh re-pulls the url Resin already has). - -Update frontend TS wrapper ipcSubscriptionRefresh (src/lib/ipc.ts L336): drop the url arg, keep name only. - -Closed loop verify: -- cargo test -p resin-core --lib (covers pure helpers if they live in resin-core; if they stay in commands, they are smoke-verified by build since cargo test -p egressapikey-app exits STATUS_ENTRYPOINT_NOTFOUND on this host — AGENTS s20) -- vitest: update ipc.test.ts: ipcSubscriptionRefresh forwards name only, no url - ---- - -## Phase P3 — subscription_add retarget to remote (commands/mod.rs) - -File: src-tauri/src/commands/mod.rs L544 - -Current state (L544): - fetch_clash_subscription(url) -> clash_yaml_to_proxies_block -> POST {name, source_type:"local", content:block, url, update_interval:"30s"} - -Rewrite to: - pub async fn subscription_add(app, name, url) -> Result<(), IpcError> - validate name (1..128, no control) + url (http(s):// prefix, <=2048 chars, AGENTS s7.5) - lock State<SidecarHandle>, build ResinClient - POST {name, source_type:"remote", url, update_interval:"30s"} directly to /api/v1/subscriptions - tracing::info!(subscription=%name, url=%url, "subscription add source_type=remote") - Ok(()) - -Why 30s: ADR-0044 kept 30s to force a fast first tick; Resin has no public force-refresh endpoint. 30s interval keeps the first background fetch within seconds, not the default 5m. - -Add 1 cargo mockito test (in resin_client.rs, next to create_subscription): -- mockito_create_subscription_remote_posts_source_type_remote: mock POST with body asserting source_type == "remote", url present, no content field. Returns 201. - -Update vitest ipc.test.ts: -- ipcSubscriptionAdd body asserts source_type:"remote" (was "local") - -Closed loop verify: -- cargo test -p resin-core --lib: +1 (mockito_create_subscription_remote) -- vitest: 1 assertion updated - ---- - -## Phase P4 — i18n: 4 error keys x 18 locales + i18n_key on IpcError variants - -### 4 new i18n keys (all 18 base locales + src/test/setup.ts en-inline) - -- error.bindConflict -> en "Port is already in use; pick a different port." / zh "端口已被占用,请更换端口重试。" -- error.invalidStrategy -> en "Unknown allocation strategy." / zh "未知的分配策略。" -- error.resinUpstream -> en "Upstream service returned an error. Retry in a moment." / zh "上游服务异常,请稍后重试。" -- error.internal -> en "Internal error. Check logs for details." / zh "内部错误,请查看日志。" - -src/test/setup.ts en-inline catalog must mirror these 4 keys (the i18n:check gate reads en/*.json as canonical). - -### IpcError i18n_key field - -File: crates/resin-core/src/ipc_error.rs - -Add field pub i18n_key: &'static str to each variant: -- BindConflict { port: u16 } -> i18n_key = "error.bindConflict" -- InvalidStrategy { detail: String } -> i18n_key = "error.invalidStrategy" -- ResinUpstream { status: u16, excerpt: String } -> i18n_key = "error.resinUpstream" -- Internal { msg: String } -> i18n_key = "error.internal" - -Update: - -- impl Serialize: the existing Serialize impl (or derived serde) must emit i18n_key so the frontend translateError->extractIpcErr can pick it up. If using derive(Serialize), no extra work; if manual, add the field to the serde::serialize impl. -- Frontend src/lib/i18n-error.ts: translateError already calls t(error.i18n_key) for BindConflict; verify that ResinUpstream/InvalidStrategy/Internal also flow through the same path. If extractIpcErr only narrows by variant tag, add a fallback that reads error.i18n_key before falling back to a generic message. - -### 2 vitest in i18n-error.test.ts - -- translateError uses error.bindConflict key with {{port}} interpolation when IpcError variant is BindConflict -- translateError falls back to error.internal for unknown variant but uses error.resinUpstream for status >= 400 - -Closed loop verify: -- pnpm i18n:check -> +4 keys x 18 locales (146 -> 150) -- pnpm test -> +2 vitest (baseline 73 -> 75 or counts vary per host) - ---- - -## Phase P5 — map_resin_error demote warn->debug + status==0 -> Internal - -### Bug 1: noisy warn on single-probe failure - -File: src-tauri/src/commands/mod.rs L58-60 - -Current: - pub fn map_resin_error(raw: &str) -> resin_core::IpcError { - tracing::warn!("Resin error mapped to i18n: {}", raw); - -Change tracing::warn! -> tracing::debug!. Single-node probe failures are routine (node offline, timeout); a warn line per failure floods the log and the user-reported bug was "log shows [wrn] Resin error mapped to i18n" during normal batch probe. Debug keeps it searchable via RUST_LOG=debug but not noisy at default INFO. - -### Bug 2: non-HTTP failures masquerade as ResinUpstream - -File: crates/resin-core/src/ipc_error.rs map_resin_error - -Current arms: - status >= 400 -> ResinUpstream - unknown -> Internal - -Add an explicit arm BEFORE the 400 arm: - status == 0 -> Internal { msg } - -This catches reqwest::Error (connect timeout, DNS, sidecar down) where the status is 0 (no HTTP response). Stops a sidecar-down IPC from surfacing as "Upstream service returned an error. Retry in a moment." which is misleading. - -Add 2 cargo tests in ipc_error.rs: -- map_resin_error_status_zero_returns_internal: assert status==0 -> IpcError::Internal -- map_resin_error_status_500_returns_resin_upstream: assert status==500 -> IpcError::ResinUpstream (regression guard) - -Closed loop verify: -- cargo test -p resin-core --lib: +2 (139 -> 140-ish) -- Smoke: Start-Process release exe, tail logs, verify NO "[wrn] Resin error mapped to i18n" appears on a single probe failure (bug-1 symptom) - ---- - -## Full-gate verification (AGENTS s5 hard close-loop) - -1. pnpm exec tsc --noEmit green -2. pnpm i18n:check: +4 keys x 18 locales (146 -> 150) -3. pnpm test: all vitest green; watch T1-4-* flaky family (pre-existing, retry) -4. cargo test -p resin-core --lib: baseline 137 -> expect ~139-140 (delete 4 P13 tests, add 4 T20 tests) -5. cargo build --release -p egressapikey-app --features custom-protocol green -6. codegraph sync . -7. bash scripts/build-all.sh -> stage release/windows-gui/EgressAPIKEY.exe + resin.exe sidecar -8. Fresh-bundle guard: grep latest Vite chunk hash in staged exe bytes (AGENTS s5 step 4) -9. Smoke: Start-Process -> MainWindowTitle=="EgressAPIKEY", WS 20-40MB, resin child alive, log tail has NO "[wrn] Resin error mapped to i18n" from single probe (bug-1 symptom). Log tail HAS "[info] subscription refresh sink to /actions/refresh" after a refresh button click (P2 signal). - -## AGENTS.md section + commit + push - -- Append section (check grep -n '^### ' AGENTS.md | tail for next number; likely s60/s61) documenting T20: refresh sinks to Resin native, P13 B4 chain deleted, 4 i18n error keys, map_resin_error demote, status==0 -> Internal. -- Commit per AGENTS s6: "T20: subscription refresh sink to Resin native /actions/refresh -- delete P13 B4 shell fetch/convert/patch + 4 i18n error keys + map_resin_error demote" -- Push. diff --git a/docs/history/phases/GRILL_T21_NODE_POOL_BUTTON_SYNC_PLAN.md b/docs/history/phases/GRILL_T21_NODE_POOL_BUTTON_SYNC_PLAN.md deleted file mode 100644 index e038e1b4..00000000 --- a/docs/history/phases/GRILL_T21_NODE_POOL_BUTTON_SYNC_PLAN.md +++ /dev/null @@ -1,164 +0,0 @@ -# GRILL T21 - Node Pool Button Sync + Batch Progress + Density Refactor Plan - -> Canon: ADR-0046 (decisions) + CONTEXT.md deltas (1 new term `nodePoolToolbar` + `nodeProbe` note) + this file (5-phase execution). -> Source grill rounds: R1 (Q1-Q4 all A) + R2 (Q5a=A / Q5b=A) + R3 (Q6a=A / Q6b=A / Q6c=A). -> Authority: ADR-0044 S1/S3/S4 still hold; ADR-0047 sets the Resin-native /actions/refresh path this plan builds on (T20-v2 already implemented). -> Branch: T21 (parallel to any audit-clean branch; tree currently clean at HEAD 1d23d74). - -## Decisions locked - -| Q | Topic | Answer | -|---|-------|--------| -| Q1 | Refresh + BatchProbe button position after T20 nested-button fix + top-right refresh semantic | **A** - outer `<button>` to `<div + onClick>`; merge the two action buttons inline into the same `flex` row as the title/name/count/health - no nested `<button>`, clash-rev HeadState IconButton pattern. Top-right refresh button tooltip changes to `nodes.syncCache` ("Re-sync backend node snapshot"), separated from the sub-group refresh semantic. | -| Q2 | Batch probe progress feedback (currently tooltip pinned at `done=0`) + Refresh sub silent | **A** - batch probe merges with single-node path: shared `applyProbeResult(hash, kind, res)` writes `setProbeResults` after each probe finishes, so every row lights up `- to ms` in real-time; add `batchProgress: Map<sub, {done,total}>` state for the spinner tooltip; refresh sub shows toast `nodes.refreshSent` immediately then `await`s; on success checks node-count delta and shows `refreshDone` / `refreshNoChange`. No delayed polling (see Q5a). | -| Q3+Q4 | Density refactor + top-right refresh mental model expiration | **A** - delete the 3 info-text cards (`egressPolicyNote`, `protocolWeights` body content crammed into an `Info` popover on the StatCard corner; `reputationTitle` body moves to the footer row as one line sharing `lastRefresh`). Merge the search toolbar into one sticky-top single-line flex row (search flex-1 + 5 toggles). Top-right refresh button tooltip renamed `nodes.syncCache`. | -| Q5a | Wait strategy after Refresh sub click - async or sync? | **A** - Resin v1.2.0 `/actions/refresh` is `sync` (RefreshSubscription to Scheduler.UpdateSubscription to Fetcher(url) blocks); on return new nodes are already in memory. No delayed/polling fallback. Toast `refreshSent` immediate, `await` wraps `refresh()`, compare counts to pick `refreshDone` / `refreshNoChange`. Simpler than Q2-A provisional 1.5s/3s schedule. | -| Q5b | i18n key naming convention | **A** - flat under `nodes.*` namespace. 5 new keys: `syncCache` / `refreshSent` / `refreshDone` / `refreshNoChange` / `batchProgress`. All verified unused in current 37 `nodes.*` keys. | -| Q6a | Test closed-loop form | **A** - pure helpers (`applyProbeResult` + `nextBatchProgress`) plus component mock path; +5 vitest assertions; no e2e (follow existing NodesView test pattern); no new cargo (truth is already covered by T20 mockito `refresh_subscription_native`). | -| Q6b | ADR + Plan + AGENTS.md placement | **A** - Plan `docs/GRILL_T21_NODE_POOL_BUTTON_SYNC_PLAN.md`, ADR `docs/adr/0046-node-pool-refresh-batch-progress-button-layout.md`, CONTEXT.md delta one new term + note on `nodeProbe`; AGENTS.md append section 63. | -| Q6c | Build closed-loop form | **A** - `tsc --noEmit` + `pnpm i18n:check` (+5 keys x 18 locales = +90 entries, 388 to 393) + `pnpm test` (+5 vitest) + `cargo test -p resin-core --lib` (baseline unchanged) + `pnpm build` (new chunk hash) + touch `src-tauri/src/main.rs` + `cargo build --release -p egressapikey-app --features custom-protocol` + chunk-hash verify in staged exe + smoke + `codegraph sync .` + `git push`. | - -## Context (prior grill history this layer builds on) - -- **ADR-0044** S1 (default-collapse + hide-unhealthy + latency sort) - untouched. S2 (shell-side subscription refresh via local re-fetch) - superseded by ADR-0047. S3 (Resin-native per-node probe) - foundation. S4 (whitebox probe knobs) - foundation. -- **ADR-0047** (T20-v2): subscription refresh sinks to Resin-native `POST /api/v1/subscriptions/{id}/actions/refresh` with `source_type=remote` on add. Resin `Scheduler.UpdateSubscription` for remote source_type is **sync** and blocks on `Fetcher(url)` returning; new nodes already in Resin memory when the POST responds. Q5a-A realises this synchronously. -- **ADR numbering resolved**: ADR-0045 (IPC error contract) and ADR-0047 (subscription refresh) are now separate numbers (was previously a double-0045 conflict, resolved in T21-audit commit). - -## Phase execution plan - -### Phase 1 - Layout fix: inline button row + top-right refresh tooltip rename - -**Files**: `src/views/NodesView.tsx` L400-427 (button to div + inline flex) - -**Work**: -1. Change L401 `<button onClick={() => toggleSub(sub)} ...>` to `<div onClick={...} role="button" tabIndex={0} className="...">`. Keep the chevron + name + count + health-rate children layout unchanged. Add `onKeyDown` handler (Enter/Space) for keyboard access (replaces native `<button>` semantics). -2. Inline the two existing action buttons (L411-418 RefreshCw + L419-426 Activity) into the same row right of the health-rate span: delete L410 outer `<div className="flex items-center gap-1 px-4 pb-1 -mt-1">` and move the two buttons up into L401 row. The row becomes a single `flex items-center gap-2` containing chevron + name + node-count + health-rate + 2 action icons. -3. Delete L410-427 outer wrapper div since buttons now in-row. -4. Top-right refresh L300-306: change button title from `t("nodes.refresh")` to `t("nodes.syncCache")` (i18n key added Phase 5). Keep the RefreshCw icon. The semantic shift is tooltip-only; the click action still calls `refresh()` (re-read Resin memory). - -**i18n keys** (added Phase 5, all 18 locales): -- `nodes.syncCache`: en="Re-sync backend node snapshot" / zh="重新同步后端节点快照" - -**Loop test**: Phase 2 vitest assertion that the row has no nested `<button>` children and 2 IconButtons (RefreshCw + Activity) as siblings. - -**Estimate**: ~25 lines TS changed (mostly mechanical move + `<button>` to `<div>`). - -### Phase 2 - Batch probe progress (real-time per-probe light-up + tooltip done/total) - -**Files**: `src/views/NodesView.tsx` L194-226 `handleBatchProbe` + L132 `probeResults` + new `batchProgress` state - -**Work**: -1. Extract pure helper `applyProbeResult(prev: Map, hash: string, kind, res) -> Map` from the inline block in `handleProbeNode` L174-185. Both single-probe (L173 path) and batch-probe (L206 path) now call this helper via `setProbeResults(prev => applyProbeResult(prev, hash, kind, res))`. This unifies the write-path so batch actually posts to `probeResults` (the missing wire today). -2. Add `const [batchProgress, setBatchProgress] = useState<Map<string, {done:number, total:number}>>(new Map())` state. -3. In `handleBatchProbe`: - - Before the `for` loop: `setBatchProgress(prev => new Map(prev).set(subName, {done:0, total: hashes.length}))`. - - In the `chunk.map` body: after `await Promise.race([probe, timeout])` try-catch, do `setProbeResults(prev => applyProbeResult(prev, hash, "latency", res))` where `res` is the resolved probe; if `Promise.race` throws, `res` stays `undefined` so `applyProbeResult` short-circuits (no-op). - - After each `done++`: `setBatchProgress(prev => { const m = new Map(prev); const cur = m.get(subName) ?? {done:0,total:hashes.length}; m.set(subName, {done: cur.done+1, total: cur.total}); return m; })`. -4. Tooltip L420 `t("nodes.batchProbing", { done: 0, total: items.length })` to `t("nodes.batchProbing", batchProgress.get(sub) ?? {done:0, total: items.length})`. `batchProbing` already parametric on `{{done}}` / `{{total}}` - verify before Phase 5 key add. -5. On `finally`: `setBatchProgress(prev => { const m = new Map(prev); m.delete(subName); return m; })` (clean reset). - -**i18n**: `nodes.batchProgress` is the tooltip string for the progress spinner itself - already seconded by `batchProbing` interpolating `{{done}}/{{total}}`; if `batchProbing` already covers it we verify there is no key gap. Add `batchProgress` only if Phase 2 outcome needs it; otherwise drop to keep YAGNI. - -**Loop test** (vitest, `src/views/NodesView.test.tsx` +3 assertions): -- `applyProbeResult` pure: idempotent on same hash; `kind=egress` adds `egress_ip+region` fields without clobbering existing `latency`; `kind=latency` only updates `latency`. -- `nextBatchProgress` pure (the increment helper): `{done:N+1,total:M}` from `{done:N,total:M}` on a fired probe; reset returns `undefined` to fall back to original `batchProbe` label. -- Component mock: render NodesView, click `batchProbe` button on a sub group with 2 fake nodes, assert `ipcNodeProbe` called twice via mocked IPC, and `applyProbeResult` to `probeResults` state observed on both rows (latency = 50ms, 100ms). - -**Estimate**: ~50 lines TS changed + 2 new pure helpers + 3 vitest. - -### Phase 3 - Refresh sub toast feedback (sync backend fact-driven) - -**Files**: `src/views/NodesView.tsx` L152-167 `handleRefreshSub` - -**Work**: -1. Track pre-click node count: `const before = nodes.length` captured at click entry. -2. Show toast-like inline state first: `setLocalToast({key: "refreshSent"})` - new state `const [localToast, setLocalToast] = useState<{key:string, opts?:object} | null>(null)`, auto-clear after 2s via `setTimeout`. Render the toast in the NodesView top area near the error banner slot. -3. `const nodeDelta = await ipcSubscriptionRefresh(subName); await refresh(); const after = nodes.length; const delta = after - before;` -4. If `delta > 0` to `setLocalToast({key:"refreshDone", opts:{count: after}})`. Else to `setLocalToast({key:"refreshNoChange"})`. Both auto-clear after 3s. -5. Keep the existing `refreshingSub` Set state for the spinner - no change to that skeleton. -6. On error path: keep existing `translateError(e, t) to setError` (already in place). Add `setLocalToast(null)` on catch. - -**i18n keys** (Phase 5, all 18 locales): -- `nodes.refreshSent`: en="Refresh request sent to backend" / zh="刷新请求已发送到后端" -- `nodes.refreshDone`: en="Refreshed, {{count}} nodes in pool" / zh="已刷新,当前共 {{count}} 个节点" -- `nodes.refreshNoChange`: en="No node count change; retry might be needed" / zh="节点数未变化,可稍后重试" - -**Loop test** (vitest, +2 assertions): -- `handleRefreshSub` mock path: click Refresh sub button to `ipcSubscriptionRefresh` called once to `setLocalToast({key:"refreshSent"})` shown, then after `refresh()` (mocked) returns to `setLocalToast({key:"refreshDone",opts:{count:42}})`. -- `handleRefreshSub` zero-delta path: mocked `nodes.length` unchanged after refresh to `setLocalToast({key:"refreshNoChange"})`. - -**Estimate**: ~30 TS, 3 i18n keys x 18 = 54 entries. - -### Phase 4 - Density refactor (3 text cards to popover + sticky toolbar merge) - -**Files**: `src/views/NodesView.tsx` L309-322 (reputation card) + L338-347 (egressPolicyNote + protocolWeights) + L349-386 (search toolbar) + new `InfoPopover` component (co-located helper) - -**Work**: -1. Delete L309-322 standalone reputation card. Move reputation content into the L449-453 `lastRefresh` footer row: 2-column flex `flex items-center gap-2` `<footer>` - left side `reputationSummary` 1-line compact, right side `lastRefresh` time. Reputation empty / unavailable state stays 1-line with - placeholder. -2. Delete L338-347 egressPolicyNote + protocolWeights standalone cards. Crumb their body content into a small `<Info size=12 />` icon at the top-right corner of the giveaway StatCard (L324-329); `hover` opens a popover (`z-50 whitespace-pre-line`) showing both notes. Ponytail: reuse an existing `<Info>` tooltip pattern from elsewhere in the repo if any; otherwise define inline (max ~30 lines). No new dependency. -3. Promote L349-386 search toolbar (search input + 5 toggle chips + expand/collapse-all) to `sticky top-0 z-30 bg-white dark:bg-zinc-950 pb-1` so it stays visible while the node list scrolls. Keep the same flex wrap behaviour for mobile. -4. No changes to `VIRTUAL_THRESHOLD = 50` or the column layout (Ponytail: column reflow is bigger change beyond this scope). - -**i18n**: no new keys (cards deleted, content reused in popover/footer; helper labels like `nodes.protocolWeights` already exist). - -**Loop test** (vitest, +3 assertions): -- Render nodes view with reputation data to assert `lastRefresh` row contains reputation summary AND timestamp. -- Render nodes view to assert no standalone `egressPolicyNote` card div exists in output; assert Info icon in StatCard corner is present. -- Sticky toolbar: jsdom does not compute sticky positioning but we can assert `sticky top-0` className on the toolbar wrapper. - -**Estimate**: ~60 TS, 0 new Rust, 0 i18n keys. - -### Phase 5 - i18n seed + AGENTS section 63 + full gate build - -**Files**: `src/locales/*/common.json` (18 files, each +4 to +5 keys), `src/test/setup.ts` en-inline mirrors 4 keys, `AGENTS.md` + section 63, `codegraph sync .` - -**Work**: -1. Add 5 new keys to all 18 `common.json` files under `nodes` namespace: - - `nodes.syncCache` (Phase 1) - - `nodes.batchProgress` (Phase 2 formal fallback label; check if Phase 2 actually reuses it - if not, drop it to keep YAGNI; final add based on Phase 2 outcome) - - `nodes.refreshSent` (Phase 3) - - `nodes.refreshDone` (Phase 3) - - `nodes.refreshNoChange` (Phase 3) -2. Run `pnpm i18n:check` - expect 388 to 393 keys x 18 locales. -3. `pnpm exec tsc --noEmit` green. -4. `pnpm test` - expect baseline + 5 (Phase 2 has 3 pure + 2 component + Phase 3 has 2 component + Phase 4 has 3 component; final count per Phase outcome). -5. `cargo test -p resin-core --lib` - baseline unchanged (no new Rust). -6. `pnpm build` (tsc + vite) - new Vite chunk hash. -7. Touch `src-tauri/src/main.rs` then `cargo build --release -p egressapikey-app --features custom-protocol` - exe staged at `release/windows-gui/EgressAPIKEY.exe`. -8. Grep new Vite chunk hash inside staged exe bytes (AGENTS section 5 hard close-loop). -9. Smoke: `Start-Process` to `MainWindowTitle="EgressAPIKEY"`, resin child alive, log tail no `WARN resin_ipc`. -10. `codegraph sync .` re-index. -11. Append `AGENTS.md` section 63 document block: P26-T21 node-pool button sync + batch progress + density refactor - 3 bugs fixed + 4 i18n keys x 18 locales + 5 vitest + cargo baseline unchanged + exe chunk-hash verified. - -**CONTEXT.md deltas**: -- New term `nodePoolToolbar` - the single inline flex row on each subscription-group title bar containing chevron + name + node-count + health-rate + RefreshCw + Activity IconButton. References ADR-0046. Length-by-design (Ponytail): no separate action band below the title bar; title bar IS the only surface. -- `nodeProbe` term existing note addendum: per-probe results propagate via `applyProbeResult` immediately (clash-rev DelayManager per-proxy listener model); in-batch each row updates - to ms the moment its probe resolves; batch progress number shown in spinner tooltip for at-a-glance state. -- `subscriptionRefresh` term existing note addendum (supersedes T20 partial description): the frontend pair (`handleRefreshSub` + `ipcSubscriptionRefresh`) runs synchronously with backend: toast `refreshSent` fires immediately, `ipcSubscriptionRefresh` blocks until Resin `Scheduler.UpdateSubscription` returns (remote fetch done), `refresh()` re-reads the now-fresh Resin memory, delta check to `refreshDone` / `refreshNoChange`. - -**Estimate**: ~30 docs, 5 i18n keys x 18, gate verification. - -## ADR-0046 (decisions locked) - -1. **S1 - Inline button row + title bar as single per-group toolbar (Q1-A)**: move Refresh+BatchProbe inline into the title row by swapping the outer `<button>` for `<div + onClick>`, matching clash-rev HeadState pattern. Rejected: nested `<button>` (HTML spec violation; original bug T20-v2 just fixed). Rejected: separate sibling div with negative margins (visual cheating, does not address mental model). -2. **S2 - Batch progress = per-probe immediate `setProbeResults` + Map-based tooltip count (Q2-A)**: clash-rev DelayManager per-proxy listener model - each probe completion instantly updates its own row latency value via `applyProbeResult` (extracted helper); batch tooltip runs `batchProgress` Map for progress number. Rejected: only display total progress text without per-row update (users cannot see WHICH node finished). Rejected: wait until all probes done then render (no real-time visual feedback, same symptom as today bug). -3. **S3 - Refresh sub sync feedback via immediate toast (Q5a-A)**: Resin v1.2.0 `/actions/refresh` is `Scheduler.UpdateSubscription` which for remote source_type blocks on `Fetcher(url)`; when POST responds the new nodes are already in Resin memory. Toast `refreshSent` shows the click, backend blocks on the same promise, `refreshDone` / `refreshNoChange` based on node-count delta. No late-polling fallback. Rejected: 1.5s / 3s retry interval (Q2-A provisional plan dropped; adds UX wait for nothing because backend is sync). -4. **S4 - Density refactor: 3 text cards to popover / footer / sticky toolbar (Q3+Q4-A)**: delete standalone egressPolicyNote + protocolWeights cards, content into Info icon popover on StatCard corner; reputation card body into the `lastRefresh` footer row as 1-line compact; search toolbar promoted to sticky top. Top-right refresh tooltip renamed `nodes.syncCache` to separate from sub-group refresh semantic. Rejected: delete cards permanently (loses discoverability for new users); rejected: collapse-on-expand panel (still occupies row outline space compared to popover). - -## Rejected alternatives (recorded for audit trail) - -- Keep nested `<button>` and instead of div+onClick: HTML spec 4.10.1 forbids `<button>` as a descendant of another interactive element; the original bug T20-v2 just fixed this. -- Hybrid: `<button>` parent for title, `<div>` container for actions as sibling (current post-T20-v2 structure): leaves the ugly "below the title row" L410-427 wrapper stayed, which was bug #1. -- Full DelayManager single instance (Q2-C): full clash-rev DelayManager single instance takes 200+ lines of `useReducer` + `setListener` / `removeListener` lifecycle management; our per-node need is merely state-update-on-resolve, which `setProbeResults` + `applyProbeResult` helper already covers with Ponytail concision. -- 1.5s/3s delayed poll (Q5a-B): based on wrong assumption that Resin refresh is async; source-level research of `internal/topology/subscription_scheduler.go UpdateSubscription` shows it blocks on `Fetcher(url)` for remote source type. Q5a-A holds. -- Move reputation / egressPolicyNote / protocolWeights content to Settings page (Q3-C): would require i18n key migration from `nodes.*` to `settings.*` namespace across 18 locales + routing changes. Worth it only if the user later wants a separate `diagnostics` page; not in this bug-fix scope. -- Top-right refresh button delete (Q4-C): the auto-poll does the same work every 10s, but manual trigger has UX value during rapid testing; keeping it with a corrected semantic tooltip (`syncCache`) is the balance. - -## Open debt explicitly NOT addressed here - -- ADR-0045 (IPC error contract) and ADR-0047 (subscription refresh) are now distinct — resolved in the T21-audit commit. -- VirtualNodeList virtualization for >50 nodes - already works; column reflow like clash-rev is bigger work and not in this scope. -- `settings.json#nodeProbe` knobs surface in Settings panel (ADR-0044 S4 surface) - still planned but unchanged by T21. - -## Summary - -3 user-reported bugs (button position off / batch progress stuck at 0 / top-right refresh mental model confused + density cluttered) addressed with 4 surgical phases (layout, batch progress, refresh toast, density) + 5 i18n keys x 18 locales + 5+ vitest closed loops. No new IPC, no new Rust, no backend change. Total estimate ~165 TS lines changed + 5 i18n keys x 18 entries + 5 vitest + 0 cargo + docs. \ No newline at end of file diff --git a/docs/history/phases/GRILL_T22_CANVAS_A_CLASS_SEMANTIC_PLAN.md b/docs/history/phases/GRILL_T22_CANVAS_A_CLASS_SEMANTIC_PLAN.md deleted file mode 100644 index bf2cc764..00000000 --- a/docs/history/phases/GRILL_T22_CANVAS_A_CLASS_SEMANTIC_PLAN.md +++ /dev/null @@ -1,112 +0,0 @@ -# GRILL T22 — Canvas a_class-semantic C-column + new-platform default entry - -> Canon: ADR-0048 (decisions) + this file (phases). -> Source grill rounds: Q1-Q7 (all A), code-level root cause analysis via codegraph + ctx_execute_file. - -## Decisions locked - -| Q | Decision | Impact | -|---|----------|--------| -| Q1 | C-column filter by a_class, not region_filters union | buildCColumnGroups rewrite | -| Q2 | New platform auto-default strategyConfig entry in sync merge | sync() merge block +15 lines | -| Q3 | Per-platform a_class branch filtering helper | isNodeSelectedByAnyPlatform ~30 lines | -| Q4 | region + subscription viewMode share filter helper | buildCColumnGroups unify | -| Q5 | buildEdges B->C edge by a_class semantics | buildEdges rewrite ~30 lines | -| Q6 | 12 new vitest closed-loop assertions | TopologyView.test.tsx | -| Q7 | Delete getSelectedRegions + selectedRegions | -15 lines dead code | - -## Phase execution plan - -### Phase 1 — isNodeSelectedByAnyPlatform helper (Q3+Q4) - -**Files**: src/views/TopologyView.tsx - -**Work**: -1. New pure helper `isNodeSelectedByAnyPlatform(node: NodeItem, platforms: PlatformFull[], groupKey: string): boolean` -2. Logic: iterate platforms, check a_class per Q3 spec -3. Export it (for direct vitest) - -**Tests**: 4 vitest (subscription match, region match, quality all-healthy, manual hash match) - -**Est**: ~30 lines + 4 tests - -### Phase 2 — buildCColumnGroups signature + filter rewrite (Q1+Q4+Q7) - -**Files**: src/views/TopologyView.tsx, src/views/TopologyView.test.tsx - -**Work**: -1. Change buildCColumnGroups signature: `selectedRegions: Set<string>` -> `platforms: PlatformFull[]` -2. Replace selectedRegions.has(getNodeRegion(n)) with isNodeSelectedByAnyPlatform(n, platforms, groupKey) -3. Replace selectedRegions.has(region) in region viewMode (L271) with same helper -4. Delete getSelectedRegions (L104-110) -5. Remove selectedRegions useMemo (L859) -6. Update rawNodes useMemo call (L942): pass platforms instead of selectedRegions -7. Update test imports + fix existing buildCColumnGroups tests (L843-859) - -**Tests**: update 2 existing tests to pass PlatformFull[] instead of Set<string> - -**Est**: ~40 lines refactor + ~10 lines test fix - -### Phase 3 — buildEdges a_class-semantic B->C edge (Q5) - -**Files**: src/views/TopologyView.tsx, src/views/TopologyView.test.tsx - -**Work**: -1. buildEdges subscription viewMode branch (L620-641): replace region_filters-only with a_class switch -2. a_class=subscription+empty subscriptions -> edge to ALL groups -3. a_class=subscription+specific -> edge to matching subscription groups -4. a_class=region -> edge to matching region groups -5. a_class=quality -> edge to ALL groups -6. a_class=manual -> NO group-level edge -7. region viewMode edges (L960-966): same a_class switch -8. Update existing buildEdges tests (L270-316) to pass a_class field - -**Tests**: 4 new vitest (subscription+empty=all edges, subscription+specific, region, manual=no edges) - -**Est**: ~30 lines refactor + 4 tests - -### Phase 4 — sync() merge default strategyConfig entry (Q2) - -**Files**: src/views/TopologyView.tsx - -**Work**: -1. In sync() merge block (L774-792), after cfgRaw.platforms map build: iterate Resin platforms, if not in map -> auto-create default entry -2. Default: { platform_name, a_class: "subscription", b_class: "random", subscriptions: [], regions: [] } -3. This is in-memory merge only (not persisted to JSON until user configures) - -**Tests**: 1 vitest (Resin platform not in strategyConfig -> merge creates default entry) - -**Est**: ~15 lines + 1 test - -### Phase 5 — Full gate: build + i18n + test + exe smoke - -**Files**: N/A (verification only) - -**Work**: -1. `pnpm test` — all vitest green (existing 83 + 12 new = 95) -2. `pnpm exec tsc --noEmit` — clean -3. `pnpm build` -> new Vite chunk hash -4. `cargo build --release -p egressapikey-app --features custom-protocol` -> exe -5. Stage exe + resin.exe sidecar to release/windows-gui/ -6. Grep Vite chunk hash in exe bytes (stale-bundle guard) -7. Smoke launch: MainWindowTitle="EgressAPIKEY", WorkingSet ~36MB, resin.exe child alive -8. `codegraph sync .` + commit + push - -**Acceptance**: all gates green, exe fresh, smoke alive - -## Test closed-loop summary - -| Test | What it asserts | -|------|----------------| -| isNodeSelectedByAnyPlatform subscription | node in selected subscription -> true | -| isNodeSelectedByAnyPlatform region | node in selected region -> true | -| isNodeSelectedByAnyPlatform quality | healthy node -> true | -| isNodeSelectedByAnyPlatform manual | node_hash in manualNodes -> true | -| isNodeSelectedByAnyPlatform empty default | new platform subscriptions=[] -> all true | -| buildEdges subscription+empty | edges to ALL groups | -| buildEdges subscription+specific | edges to matching groups only | -| buildEdges region | edges to region groups only | -| buildEdges manual | NO group-level edges | -| buildEdges quality | edges to ALL groups | -| sync merge default entry | Resin platform not in strategyConfig -> auto-default | -| buildCColumnGroups updated signature | accepts PlatformFull[] not Set<string> | diff --git a/docs/history/phases/GRILL_T22_HEADLESS_RENDERING_FIX.md b/docs/history/phases/GRILL_T22_HEADLESS_RENDERING_FIX.md deleted file mode 100644 index 5b049e33..00000000 --- a/docs/history/phases/GRILL_T22_HEADLESS_RENDERING_FIX.md +++ /dev/null @@ -1,106 +0,0 @@ -# GRILL T22 — Headless Server Rendering Fix + WebView2 Architecture Alignment Audit - -> Status: PLANNED (post-grill, awaiting user instruction to build) -> Date: 2026-08-19 -> Branch: codex/rust-port (fix branch, parallel) -> Predecessor: T21 (headless/WebView2 architecture audit — closed) - -## Problem - -The headless npm server (egressapikey-headless.exe) starts, binds port 14200, -opens the browser to http://127.0.0.1:14200/, and the user sees a flash of -the SideRail nav + "Loading..." text before the entire React tree unmounts, -leaving a blank white page. - -## Root Cause (verified via code + atomcode research) - -src/lib/ipc.ts ipcWatchPortHealth() unconditionally constructs -new Channel<PortHealthSnapshot>() from @tauri-apps/api/core and calls -_invoke("watch_port_health", ...) without the isTauri() guard. In a -plain browser (headless mode), Channel constructor touches -window.__TAURI_INTERNALS__ which does not exist, synchronously throws -inside the React useEffect in TopologyView.tsx L916, React unmounts -the entire component tree, white screen. - -Secondary issues: -1. headless_main.rs ServeDir has no not_found_service, client-side - routes (refresh on /platforms) return 404, blank page on deep-link/refresh. -2. No React Error Boundary, any uncaught throw in any view unmounts - the entire app, no recovery, no diagnostics. - -## Grill Decisions - -| Q | Decision | Rationale | -|---|----------|-----------| -| Q1 | A | Root-path flash to white screen: React mount succeeds briefly then crashes | -| Q2 | A | headless exe starts, browser auto-opens, SideRail + Loading flash then white | -| Q3 | A | 3-layer fix: isTauri guard + ServeDir SPA fallback + Error Boundary | -| Q4 | C | agent-browser open localhost:14200, read DOM (no screenshots) | -| Q5 | A | Hand-made bash script in ctx_batch_execute, ~20 lines | -| Q6 | B | Full architecture alignment audit after white-screen fix | - -## Execution Plan - -### Phase 1 — White-Screen Fix (3 layers) - -| Step | File | Change | -|------|------|--------| -| L1 | src/lib/ipc.ts | ipcWatchPortHealth: add isTauri() guard before new Channel() | -| L2 | src-tauri/src/headless_main.rs | ServeDir not_found_service(ServeFile::new(dist/index.html)) + import ServeFile | -| L3 | src/App.tsx | ErrorBoundary class component, wrap main render | - -### Phase 2 — Build + Smoke - -| Step | Command | Gate | -|------|---------|------| -| 2a | pnpm build (tsc + vite) | tsc green, new chunk hash | -| 2b | cargo build --release --features headless | green, ~4.6MB | -| 2c | cargo build --release --features custom-protocol | green, ~14MB | -| 2d | Stage to release/windows-backend/ + release/windows-gui/ | exe + dist + resin.exe | -| 2e | Chunk hash verify in exe bytes | hash found | -| 2f | Smoke: headless exe starts, port 14200 listening | process alive | - -### Phase 3 — agent-browser Closed-Loop Verification - -| Step | Script | Assertion | -|------|--------|-----------| -| 3a | npx agent-browser open http://127.0.0.1:14200/ | page opens | -| 3b | npx agent-browser snapshot -i | SideRail nav buttons present (7 buttons) | -| 3c | npx agent-browser read | rendered DOM includes app title, #root non-empty | -| 3d | npx agent-browser snapshot -i after clicking each nav | each view renders visible content | - -### Phase 4 — Architecture Alignment Audit (headless vs webview2) - -| Step | Scope | Method | -|------|-------|--------| -| 4a | IPC command coverage | Diff CMD_TO_HTTP map (20 entries) vs all invoke() call sites. List unmapped. | -| 4b | Per-view headless compat | For each of 7 views, trace all Tauri API calls. Confirm isTauri guard or try/catch. | -| 4c | Settings persistence | settings.ts LazyStore calls: confirm fallback when not in Tauri. | -| 4d | Audit report + fix | Write findings, fix uncovered issues, delete audit doc after fix. | - -### Phase 5 — Commit + Push + Docs - -| Step | Action | -|------|--------| -| 5a | Run vitest + cargo test — green | -| 5b | Update AGENTS.md with T22 section | -| 5c | Commit: T22: headless white-screen fix | -| 5d | Push | - -## Industry References - -- tower-http 0.7.0 docs.rs: ServeDir::not_found_service + ServeFile for SPA fallback -- axum official static-file-server example: SetStatus(ServeFile, NOT_FOUND) pattern -- axum Discussion #2486: fallback(ServeFile(index.html)) community canonical answer -- React docs: Error Boundaries (getDerivedStateFromError + componentDidCatch) -- atomcode research: 14 source articles (Tavily + AnySearch), confirmed root cause - -## Acceptance Criteria - -1. egressapikey-headless.exe starts, browser opens localhost:14200, no white screen -2. SideRail visible, Topology canvas renders, all 7 tabs switchable with content -3. Page refresh on any client route returns index.html (not 404) -4. agent-browser DOM read confirms: #root non-empty, nav buttons present, app title visible -5. Error Boundary catches unexpected throws, renders fallback with reload, not white screen -6. Architecture audit: all IPC commands either mapped to HTTP route or isTauri-guarded -7. vitest + cargo test green; pnpm build + cargo build green; chunk hash verified \ No newline at end of file diff --git a/docs/history/phases/GRILL_T4_BACKLOG.md b/docs/history/phases/GRILL_T4_BACKLOG.md deleted file mode 100644 index 28a76011..00000000 --- a/docs/history/phases/GRILL_T4_BACKLOG.md +++ /dev/null @@ -1,71 +0,0 @@ -# Grill T4 Issues Backlog (2026-08-09) - -> All 5 grill questions answered. ADRs 0015-0019 created. -> CONTEXT.md updated with 6 new domain terms. - -## Decisions Summary - -| Q | Topic | Decision | ADR | -|----|------------------------------|----------|--------| -| Q1 | SOCKS5 port connectivity | Expose proxy_token to GUI + port_health_check IPC | ADR-0021 | -| Q2 | Platform egress strategies | Shell-side strategy_engine.rs (A-class + B-class) | ADR-0022 | -| Q3 | Node pool display | Collapsible tree by subscription (clash-verge-dev pattern) | ADR-0023 | -| Q4 | Settings dead field | Delete legacy dead code (laneCount + SharedGateway) | ADR-0024 | -| Q5 | Topology canvas C-column | Dynamic pool + strategy-labeled edges (Kiali pattern) | ADR-0025 | - -## Execution Order (dependency graph) - -``` -Q4 (delete dead code) --- no deps, do first -Q1 (SOCKS5 auth + health) --- no deps, parallel with Q4 -Q3 (node pool tree view) --- no deps, parallel with Q4/Q1 -Q2 (strategy engine) --- depends on Q1 (port health) for liveness gate display -Q5 (topology refactor) --- depends on Q2 (strategy data) + Q3 (node grouping) -``` - -## Phases - -### Phase T4-1: ✅ DONE Dead Code Cleanup (Q4 / ADR-0024) -- Delete laneCount from appStore.ts + SettingsView.tsx -- Delete SharedGateway / GatewayState if no other callers -- Delete AI_API_ROUTE_LANES env var + build_shared_gateway -- Test: cargo check + vitest - -### Phase T4-2: ✅ DONE SOCKS5 Auth + Port Health (Q1 / ADR-0021) -- New IPC: port_auth_info(port) -> { username, password, auth_required } -- New IPC: port_health_check(port) -> { reachable, auth_required, latency_ms } -- GUI: port cards show auth credentials + copy button + health indicator -- Test: cargo test (Rust) + vitest (GUI) - -### Phase T4-3: ✅ DONE Node Pool Tree View (Q3 / ADR-0023) -- Refactor NodesView.tsx: flat table -> collapsible tree by subscription -- Add reference_latency_ms to NodeItem interface -- Latency color coding: <200ms green, 200-500ms yellow, >500ms red, timeout gray -- Search box + row expand for details -- Test: vitest - -### Phase T4-4: ✅ DONE Strategy Engine (Q2 / ADR-0022) -- New module: crates/resin-core/src/strategy_engine.rs -- A-class: poll /nodes -> filter by strategy -> PATCH /platforms -- B-class: subscribe leases -> bias selection (random/round_robin/low_latency) -- Whitebox: egressapikey-strategy.json (hotswap-config) -- GUI: strategy selection panel per platform -- Test: cargo test (strategy logic) + vitest (GUI) - -### Phase T4-5: ✅ DONE Topology Canvas Refactor (Q5 / ADR-0025) -- C-column: subscription-folded node pool (aligned with Q3) -- B->C edges: strategy-driven with labels -- B-column: show B-class strategy name on platform nodes -- buildEdges() rewrite for strategy-aware edges -- Test: vitest (edge builder + rendering) - -## Resin v1.2.0 Source Reference -Local clone at `resin/` (gitignored). Key files: -- internal/proxy/socks5.go - SOCKS5 handler with auth -- internal/service/control_plane_endpoint.go - endpoint CRUD + validation -- internal/service/control_plane_platform.go - platform CRUD + NodeSummary -- internal/service/control_plane_nodes.go - node listing + filters -- internal/probe/manager.go - active probing (egress + latency) -- internal/platform/policy.go - AllocationPolicy enum (3 values) -- cmd/resin/endpoint_runtime.go - listener binding + demux -- cmd/resin/inbound_demux.go - first-byte protocol sniffing \ No newline at end of file diff --git a/docs/history/phases/GRILL_T5_DOMAIN_AUDIT.md b/docs/history/phases/GRILL_T5_DOMAIN_AUDIT.md deleted file mode 100644 index 8333acb6..00000000 --- a/docs/history/phases/GRILL_T5_DOMAIN_AUDIT.md +++ /dev/null @@ -1,105 +0,0 @@ -# Domain-Modeling Audit — Grill T5 Round (2026-08-10) - -## Context -Audit performed after grill Q1-Q10 decisions recorded. -Purpose: verify which decisions are already landed in code vs pending. - -## CONTEXT.md Terms Audit - -| Term | In CONTEXT.md | In Code | Status | -|------|---------------|---------|--------| -| Entry Port | Yes | Resin /api/v1/endpoints (create_endpoint, list_endpoints) | ✅ Aligned | -| Platform | Yes | Resin /api/v1/platforms (CRUD via resin_client.rs) | ✅ Aligned | -| Account | Yes | Resin identity_v1.go parseV1PlatformAccountIdentity | ✅ Aligned | -| Lease | Yes | Resin routing/lease.go LeaseTable (key=account, val=lease+NodeHash) | ✅ Aligned | -| Subscription | Yes | resin_client.rs create_subscription/list_subscriptions + clash YAML fetch | ✅ Aligned | -| Node | Yes | resin_client.rs list_nodes / list_nodes_for_platform | ✅ Aligned | -| Ghost Safety Net | Yes | sidecar.rs spawn_health_poll (3-fail → tray red + clear OS proxy) | ✅ Aligned | -| Sidecar | Yes | sidecar.rs boot_resin + SidecarHandle + RunEvent::Exit kill | ✅ Aligned | -| Egress IP Policy | Yes | strategy.rs StrategyId::to_resin_allocation_policy | ✅ Aligned | -| Topology Canvas | Yes | TopologyView.tsx 3-column A/B/C canvas | ✅ Aligned | -| Strategy Layer | Yes | strategy_engine.rs AClassStrategy (4 variants) + BClassStrategy | ⚠️ Partial — B-class not fully wired to Resin | -| AI Stream Sensor | Yes | No code yet (planned as independent pluggable module) | ❌ Not landed | -| IP Reputation Provider | Yes | No code yet (planned: IPQualityScore/AbuseIPDB/ip-api) | ❌ Not landed | -| Protocol Weight | Yes | strategy.rs protocol_weight() static table | ✅ Aligned | -| Backup | Yes | commands/mod.rs backup_create/backup_upload (P14 path-traversal fixed) | ✅ Aligned | -| Entry Port Mapping | Yes | DbPool + SQLite (PRAGMA user_version) for port→platform mapping | ⚠️ Schema exists, port_forwarder wiring partial | -| hotswap-config | Yes | No code yet (ADR-0012 decided, not implemented) | ❌ Not landed | -| RunningMode | Yes | sidecar.rs RunningMode enum + ArcSwap<State> + crash restart (T2) | ✅ Aligned | -| Ring Buffer | Yes | sidecar.rs CommandEvent ring buffer (T2-Q1) | ✅ Aligned | -| Crash Restart | Yes | sidecar.rs crash restart with bounded retry (T2-Q3) | ✅ Aligned | -| Two-Phase Shutdown | Yes | sidecar.rs RunEvent::Exit → child.kill() (P22 orphan fix) | ✅ Aligned | -| Port Cleanup | Yes | sidecar.rs cleanup on exit | ✅ Aligned | -| Upstream Manifest | Yes | docs/RESIN_UPSTREAM_MANIFEST.yaml (ADR-0017) | ✅ Aligned | -| Read Retry | Yes | resin_client.rs send_read() with auto-retry (ADR-0018) | ✅ Aligned | -| A-Class Strategy | Yes | strategy_engine.rs AClassStrategy enum (Manual/Region/Quality/Subscription) | ✅ Aligned | -| B-Class Strategy | Yes | strategy.rs StrategyId enum (6 variants → 3 Resin policies) | ⚠️ 6→3 mapping documented but scheme c (Q10) not landed | -| Strategy Engine | Yes | strategy_engine.rs (393 lines, 9 cargo tests) | ✅ Aligned | -| Port Auth Info | Yes | commands/mod.rs port_auth_info IPC (T4-2) | ✅ Aligned | -| Port Health Check | Yes | commands/mod.rs port_health_check IPC (T4-2) | ⚠️ Protocol-aware prober (Q9) not landed | -| Strategy-Labeled Edge | Yes | TopologyView.tsx buildEdges with strategy labels (T4-5) | ✅ Aligned | - -## ADR Audit - -| ADR | Title | Status | Code Landed? | -|-----|-------|--------|-------------| -| 0001 | Shell forwarding vs Resin webui port | SUPERSEDED | N/A | -| 0002 | Topology canvas three-column | ACCEPTED | ✅ TopologyView.tsx | -| 0003 | Key+endpoint identification | SUPERSEDED by 0012 | Code deleted per ADR-0014 | -| 0004 | Egress policy surface | SUPERSEDED | N/A | -| 0005 | Q5-Q9 cluster | SUPERSEDED | N/A | -| 0006 | Mainline A then B | ACCEPTED | ✅ Executed | -| 0007 | Subscription last_error surface | ACCEPTED | ✅ SubscriptionsView refreshWithRetry | -| 0008 | Item3 live sidecar e2e | ACCEPTED | ✅ release exe smoke | -| 0009 | CLI-GUI alignment status | ACCEPTED | ⚠️ Partial (no npm install path yet) | -| 0010 | Release strategy B3 dual track | ACCEPTED | ⚠️ CI manual trigger only (A7) | -| 0011 | Observed key pool SQLite B-B-3 | SUPERSEDED by 0012 | Code deleted per ADR-0014 | -| 0012 | Route correction thin-shell multi-port | ACCEPTED | ⚠️ port_forwarder exists, endpoint API wired, no multi-port listener yet | -| 0013 | Project rename to EgressAPIKEY | ACCEPTED | ✅ All renamed | -| 0014 | Dead code deletion interceptor/route_id | ACCEPTED | ✅ interceptor.rs deleted, route_id gone | -| 0015 | Resin v1.2.0 upgrade port_forwarder downgrade | ACCEPTED | ⚠️ port_forwarder downgraded to 5 IPC endpoint API | -| 0016 | Hand-rolled sidecar lifecycle | ACCEPTED | ✅ sidecar.rs (T2) | -| 0017 | Upstream Resin version manifest | ACCEPTED | ✅ RESIN_UPSTREAM_MANIFEST.yaml | -| 0018 | Read-only IPC auto-retry | ACCEPTED | ✅ resin_client.rs send_read() | -| 0019 | build-all.ps1 + release standardization | ACCEPTED | ⚠️ build-all.ps1 exists but bug 1 (duplicate) | -| 0020 | Test per capability closed loop | ACCEPTED | ✅ Enforced | -| 0021 | SOCKS5 auth + port health | ACCEPTED | ✅ port_auth_info + port_health_check | -| 0022 | Shell-side strategy engine | ACCEPTED | ✅ strategy_engine.rs | -| 0023 | Node pool tree view | ACCEPTED | ✅ NodesView.tsx | -| 0024 | Delete laneCount dead code | ACCEPTED | ✅ laneCount removed from appStore + Settings | -| 0025 | Topology dynamic pool edges | ACCEPTED | ✅ TopologyView.tsx | -| 0026 | (pending) accountId-encoded B-class + IpcError trace | TO CREATE | ❌ Not landed | - -## Code-Level Verification Summary - -### Already Landed (T4 + earlier): -- T4-1: Dead code cleanup (laneCount, SharedGateway) ✅ -- T4-2: port_auth_info + port_health_check IPC ✅ -- T4-3: NodesView collapsible tree by subscription ✅ -- T4-4: strategy_engine.rs A-class 4 variants ✅ -- T4-5: TopologyView strategy-labeled edges ✅ -- T2: Sidecar lifecycle (RunningMode, RingBuffer, CrashRestart, Two-Phase Shutdown) ✅ -- T1: Port forwarding endpoints IPC ✅ -- P22: Orphan sidecar fix ✅ -- P13: Subscription fetch UA + flow→block convert ✅ - -### NOT Landed (This Grill Round Q1-Q10): -1. **Q1-Q5 trace_id infrastructure**: 0 occurrences of trace_id, invokeWithTrace, #[instrument], info_span -2. **Q6-Q8 IpcError contract**: 0 occurrences of IpcError, BindConflict, InvalidStrategy -3. **Q9 port_health_check protocol-aware**: exists but sends SOCKS5 greeting for ALL protocols -4. **Q10 B-class scheme c**: accountId encoding B-class strategy hint not implemented -5. **Bug 1**: build-all.ps1 duplicate headless exe -6. **Bug 2**: PlatformsView b_class default "balanced" (should be "random") -7. **Bug 3**: bind conflict raw error (will be handled by IpcError::BindConflict) -8. **Bug 4**: HTTP health check + SOCKS5 creds display for all - -### Gaps in CONTEXT.md: -- No term for "trace_id" / "trace span" — should add when infra lands -- No term for "IpcError" — should add when contract lands -- "AI Stream Sensor" term exists in CONTEXT.md but NO code — accurate (planned, not started) -- "hotswap-config" term exists but NO code — accurate (planned, not started) - -## Conclusion -The grill T5 round (Q1-Q10) is fully planned but NONE of the execution has started. -All 4 bugs + trace infrastructure + IpcError contract + B-class scheme c are pending. -The plan table below captures the full execution scope. diff --git a/docs/history/phases/GRILL_T5_TRACE_IPC_BACKLOG.md b/docs/history/phases/GRILL_T5_TRACE_IPC_BACKLOG.md deleted file mode 100644 index 07c6e48a..00000000 --- a/docs/history/phases/GRILL_T5_TRACE_IPC_BACKLOG.md +++ /dev/null @@ -1,91 +0,0 @@ -# Grill T5 Issues Backlog — Trace Skeleton + IpcError Contract (2026-08-10) - -> All 10 grill questions answered. ADR-0026 to be created. -> This grill round covers enterprise trace infrastructure + IPC error -> contract + 4 bug fixes + B-class strategy alignment. -> ADR-0012 thin-shell multi-port route correction is the architecture baseline. - -## Decisions Summary - -| Q | Topic | Decision | ADR | -|----|---------------------------------|----------|--------| -| Q1 | Trace skeleton priority | B — enterprise trace first, bugs second | (this) | -| Q2 | Trace mechanism | A — lightweight trace_id passthrough (no OTel SDK) | (this) | -| Q3 | Trace injection path | Path 甲 — __trace_id in args + Rust helper extract (path 丙 Tauri2 header not supported per PWM) | (this) | -| Q4 | Trace ID format | a — UUID v4 via crypto.randomUUID() | (this) | -| Q5 | Span layers | b — double-layer span (command info_span + ResinClient #[instrument]) | (this) | -| Q6 | IPC error contract | C — full IpcError enum + all 44 commands + GUI variant-driven i18n | ADR-0026 | -| Q7 | IpcError serialization | c — IpcError enum + i18n_key inline + externally-tagged serde | ADR-0026 | -| Q8 | Test coverage depth | b — 23 new tests (14 cargo + 9 vitest) covering every error path | (this) | -| Q9 | Port health prober | a — protocol-aware prober (SOCKS5 greeting vs HTTP CONNECT vs no-auth) + GUI conditional credentials display | (this) | -| Q10| B-class strategy alignment | c — accountId encodes B-class strategy hint (port-based, no interceptor, no route_id) | ADR-0026 | - -## Bug Root Causes (Code-Level Verified) - -### Bug 1: Duplicate headless exe -**Root cause**: `scripts/build-all.ps1` Step 5 `$bundles` uses `-match "EgressAPIKEY"` (PowerShell -match is case-insensitive + substring). It catches both `target/.../release/egressapikey-headless.exe` (hyphen) AND `target/.../release/deps/egressapikey_headless.exe` (underscore in deps/). -**Fix**: Exact match `Name -eq "egressapikey-headless.exe"` + exclude `deps/` directory. -**No grill needed — direct fix.** - -### Bug 2: "strategy config invalid: unknown variant `balanced`" -**Root cause**: `crates/resin-core/src/strategy.rs` `StrategyId::parse` accepts `random/sequential/latency/quality/bandwidth/protocol_weight` but NOT `balanced` (Resin's native value). `PlatformsView.tsx` L86/L456 defaults `b_class: "balanced"`, which parse rejects. -**Fix**: Q7 IpcError::InvalidStrategy + ADR-0026 alignment of GUI default value. - -### Bug 3: POST /endpoints 409 bind conflict -**Root cause**: Port already in use; raw EADDRINUSE error string bubbles up with no i18n handling. -**Fix**: Q6 map_resin_error → IpcError::BindConflict { port } → GUI i18n friendly message. - -### Bug 4: HTTP port health check uses SOCKS5 greeting + shows SOCKS5 creds for all -**Root cause**: `src-tauri/src/commands/mod.rs:1757` `port_health_check` sends SOCKS5 3-byte greeting `05 01 02` for ALL protocols. `PortAuthInfo` doc says SOCKS5 username/password always filled because RESIN_PROXY_TOKEN forces auth. -**Fix**: Q9 — protocol-aware prober + GUI conditional display. - -## Execution Phases - -### Phase 5-1: Trace Skeleton (Q1-Q5) -- **Frontend**: `src/lib/ipc.ts` new `invokeWithTrace(cmd, args)` wrapper: generates `crypto.randomUUID()`, injects `__trace_id` into args. Each existing wrapper calls invokeWithTrace instead of raw invoke. -- **Backend**: `src-tauri/src/trace.rs` new helper: `extract_trace_id(args: &serde_json::Value) -> Option<String>` + `trace_ipc_span(cmd: &str, trace_id: &str) -> tracing::Span`. Each `#[tauri::command]` entry calls `let span = trace_ipc_span("platform_add", &trace_id).entered();`. -- **ResinClient**: each method decorated with `#[instrument(skip(self))]`. -- **Tests**: 6 cargo (trace_id extraction 4 + instrument span 2) + 3 vitest (invokeWithTrace always carries trace_id, UUID format, args injection). -- **No new crates** — tracing + uuid already in deps. - -### Phase 5-2: IpcError Contract (Q6-Q8) -- **Rust**: New `IpcError` enum in `crates/resin-core/src/ipc_error.rs`: - ```rust - pub enum IpcError { - BindConflict { port: u16 }, - InvalidStrategy { value: String, accepted: Vec<String> }, - ResinUpstream { status: u16, excerpt: String }, - Internal { msg: String }, - } - ``` - Each variant carries `i18n_key: &str` + template params. Externally-tagged serde. -- **map_resin_error**: in `resin_client.rs` or `commands/mod.rs`, map Resin HTTP 409+wildcard "bind" → BindConflict, 400 "must be BALANCED" → InvalidStrategy, other 4xx/5xx → ResinUpstream. -- **All 44 commands**: return `Result<T, IpcError>` instead of `Result<T, String>`. -- **Frontend**: `src/lib/ipc.ts` new `extractIpcErr(e: unknown): IpcError` + discriminated union + i18n key routing. -- **Tests**: 8 cargo (IpcError serde round-trip 4 variants + map_resin_error 6 branches) + 6 vitest (extractIpcErr 4 variants + i18n_key + template params). -- **i18n**: add error keys to all 18 locales. - -### Phase 5-3: Bug Fixes (Bug 1-4) -- Bug 1: `scripts/build-all.ps1` exact match fix (~5 lines). -- Bug 2: `PlatformsView.tsx` default `b_class` from `"balanced"` to `"random"` (aligned with strategy.rs). -- Bug 3: already handled by IpcError::BindConflict from Phase 5-2. -- Bug 4: `port_health_check` protocol branch (SOCKS5 greeting / HTTP CONNECT / no-auth probe) + GUI conditional creds display (~50 lines). - -### Phase 5-4: B-Class Strategy Scheme C (Q10 / ADR-0026) -- **Architecture**: accountId encodes B-class strategy hint. When the shell creates a port mapping (port → platform + account), it encodes the B-class strategy into the account string. Resin receives the account value and creates a lease per unique account → different exit IPs for different strategies on the same platform. -- **Account format**: `<port_label>::<strategy_tag>` where strategy_tag is one of: - - `random` — Resin allocation_policy BALANCED (random node selection) - - `rr-N` — round-robin with N rotations (shell creates N accounts, rotates per request) - - `latency` — Resin PREFER_LOW_LATENCY - - `fixed` — single IP, no rotation, one account -- **Shell-side**: `strategy_engine.rs` BClassStrategy enum + account_for_bclass() function. -- **GUI**: PlatformsView B-class selector sends the strategy tag; strategy_engine generates appropriate account string when creating port mappings. -- **Tests**: 4 cargo (account generation + strategy mapping + lease isolation + round-robin rotation) + 3 vitest (GUI B-class selector + account display). -- **No fork of Resin** (ADR-0012). No interceptor, no route_id (ADR-0014). Account string is opaque to Resin; the shell owns strategy semantics. - -## Resin v1.2.0 Source Reference (Verified) -- `resin/internal/proxy/identity_v1.go`: parseV1PlatformAccountIdentity parses `Platform.Account` or `Platform:Account`. Account is an opaque string to Resin. -- `resin/internal/routing/router.go:89`: `if account == "" { random route } else { routeSticky(plat, state, account, target) }`. -- `resin/internal/platform/policy.go:7-9`: AllocationPolicy = BALANCED | PREFER_LOW_LATENCY | PREFER_IDLE_IP (3 values only). -- `resin/internal/routing/lease.go`: LeaseTable key = account. Different accounts get different leases → different exit IPs. -- `resin/internal/service/control_plane_endpoint.go:48-57`: CreateEndpointRequest has NO account field. Port IS the entry point; account comes from Proxy-Authorization credential. diff --git a/docs/history/phases/GRILL_T6_NETWORK_LAYER_PLAN.md b/docs/history/phases/GRILL_T6_NETWORK_LAYER_PLAN.md deleted file mode 100644 index 6b4c2fac..00000000 --- a/docs/history/phases/GRILL_T6_NETWORK_LAYER_PLAN.md +++ /dev/null @@ -1,119 +0,0 @@ -# GRILL T6: Network Layer — Grill Decisions & Execution Plan - -Date: 2026-08-12 -Status: GRILL COMPLETE — awaiting user "start building" instruction -Grill round: T6 (Q1-Q5) - -## Grill Decisions Summary - -| Q | Topic | Decision | Resin fork? | Shell-side | Core-side | -|---|-------|----------|-------------|------------|-----------| -| Q1 | DNS configuration exposure | A: whitebox JSON add dns_upstreams[] -> sidecar.rs inject RESIN_NODE_DNS_UPSTREAMS -> GUI Settings DNS editor + "reset to default" | No | Yes | No | -| Q2 | Network-layer whitebox config | A: extend WhiteboxConfig add network{dns_upstreams, max_idle_conns, max_idle_conns_per_host, idle_conn_timeout, probe_timeout, probe_concurrency, proxy_bypass} -> sidecar.rs inject -> Settings "Network Layer" card | No | Yes | No | -| Q3 | End-to-end test tool | C: dual-layer — unit test (mockito local echo server) + integration test (#[ignore] real 1.1.1.1 via sidecar) + GUI "Diagnostics" button (port_health_check + real exit IP probe to http://1.1.1.1/cdn-cgi/trace) | No | Yes | No | -| Q4 | Network-layer gaps coverage | Shell does #1 (Windows firewall detect) + #2 (GUI read Resin API show 0-nodes-available) + #4 (IPC read Resin request_logs DB show exit IP per request). #3 YAGNI. #5/#6/#7/#8 already covered by Resin natively or Q1/Q2. | No | Yes | No | -| Q5 | Stability + IPC + latency | B: A (gap fill: IpcError ResinUpstream full wiring + sidecar restart banner + port rebind sync) + B (network diagnostics GUI panel: sidecar PID/port/healthz/IPC latency/request log tail) | No | Yes | No | - -## Codebase Audit Facts (verified via ctx_execute + ctx_batch_execute) - -### Resin v1.2.0 already provides (NO fork needed) -- RESIN_NODE_DNS_UPSTREAMS: DoH failover chain (4 defaults), envStringSlice -- RESIN_PROXY_TRANSPORT_MAX_IDLE_CONNS (1024), MAX_IDLE_CONNS_PER_HOST (64), IDLE_CONN_TIMEOUT (90s) -- RESIN_PROXY_BYPASS: proxy bypass rules (envDelimitedStringSlice) -- RESIN_PROBE_TIMEOUT (15s), RESIN_PROBE_CONCURRENCY (1000) -- RESIN_RESOURCE_FETCH_TIMEOUT (30s) -- RESIN_DEFAULT_PLATFORM_STICKY_TTL (7d) -- RESIN_GEOIP_UPDATE_SCHEDULE ("0 7 * * *") -- Request log system: request_logs SQLite DB with egress_ip field, auto-rotation, auto-cleanup, memory queue -- Lease persistence: leases table in state.db, survives restart -- 503 + X-Resin-Error: NO_AVAILABLE_NODES when all exit nodes fail -- IPv6 support in core - -### Shell current state (verified) -- sidecar.rs injects 7 RESIN_* env vars (ADMIN_TOKEN, PROXY_TOKEN, AUTH_VERSION, LISTEN_ADDRESS, PORT, STATE_DIR, CACHE_DIR, LOG_DIR) -- whitebox_config.rs: WhiteboxConfig{version, entry_ports} — only ports, no network/DNS -- port_health_check IPC: TCP connect + SOCKS5 greeting / HTTP GET probe, has 1 cargo test -- resin_client.rs: 20 async methods (platform/subscription/node/endpoint CRUD + leases + pool snapshot) -- IpcError enum exists (BindConflict, InvalidStrategy, ResinUpstream, Internal) — NOT fully wired to all 44 commands (P5 leftover) -- sidecar-status event exists (healthy/unhealthy) — no "restarting" state -- Crash restart with bounded backoff (3x, 1s/2s/4s) — implemented -- Orphan kill on RunEvent::Exit — P22 fix, implemented -- Shell DB (egressapikey.db) and Resin DB (state.db/cache.db) separate — no SQLite lock contention - -### Enterprise template references (pwm pro gpt56_sol) -- clash-verge-rev: whitebox YAML exposes dns.enable/listen/enhanced-mode/fake-ip/nameserver/fallback -- sing-box: JSON dns.servers/rules/strategy -- Industry pattern: curl --socks5-hostname 127.0.0.1:PORT http://1.1.1.1/cdn-cgi/trace for e2e proxy test -- Tauri sidecar localhost REST: 1-5ms latency, control-plane only, no data-plane impact -- clash-verge-rev shell-to-core: REST + event-driven (not gRPC) - -## Execution Plan (dependency graph) - -T6-1 (WhiteboxConfig network + DNS extension) - -> T6-2 (sidecar.rs env injection) - -> T6-3 (GUI Settings network + DNS card) - -> T6-4 (E2E test: unit mockito + integration #[ignore] + GUI diagnostics button) [parallel with T6-5] - -> T6-5 (Windows firewall detect + 0-nodes banner + request log tail IPC) [parallel with T6-4] - -> T6-6 (IpcError ResinUpstream full wiring + restart banner + port rebind sync) - -> T6-7 (Network diagnostics GUI panel: sidecar PID/port/healthz/IPC latency/request log tail) - -> T6-8 (Closed-loop tests: cargo + vitest per capability) - -> T6-9 (Build + smoke + codegraph sync + push) - -## Item Detail - -### T6-1: WhiteboxConfig network + DNS extension -- File: crates/resin-core/src/whitebox_config.rs -- Add to WhiteboxConfig struct: pub network: NetworkConfig (serde default) -- New struct NetworkConfig with dns_upstreams, max_idle_conns, max_idle_conns_per_host, idle_conn_timeout_secs, probe_timeout_secs, probe_concurrency, proxy_bypass -- validate(): dns_upstreams non-empty if set, max_idle_conns >= 1, probe_concurrency 1..=10000 -- Test: cargo test whitebox_config network validation -- ADR: new ADR-0028 - -### T6-2: sidecar.rs env injection -- File: src-tauri/src/sidecar.rs -- After RESIN_LOG_DIR, read WhiteboxConfig network section and inject 7 env vars conditionally -- Test: cargo test sidecar env injection conditions - -### T6-3: GUI Settings network + DNS card -- File: src/views/SettingsView.tsx -- New card "Network Layer" with editable fields + "Reset to Default" button -- i18n: network.* keys across 18 locales -- Test: vitest SettingsView network card renders + save round-trip - -### T6-4: E2E test tool -- Rust unit test: crates/resin-core/tests/proxy_e2e.rs with mockito local echo server -- Rust integration test: #[ignore] test_probe_real_1_1_1_1 via sidecar -- GUI: Diagnostics button calling ipcProbeExitIp -> shows exit IP + latency -- New IPC: probe_exit_ip(port, protocol) -> reqwest through proxy to http://1.1.1.1/cdn-cgi/trace -- Test: cargo test proxy_e2e + vitest diagnostics button - -### T6-5: Windows firewall + 0-nodes banner + request log tail -- Windows firewall: new IPC check_firewall_status() -> PowerShell Get-NetFirewallProfile (read-only) -- 0-nodes banner: TopologyView reads ipcNodePoolSnapshot, red banner if 0 healthy nodes -- Request log tail: new IPC request_log_tail(limit) -> reads Resin request_logs DB -- Test: cargo test request_log_tail + vitest 0-nodes banner - -### T6-6: IpcError full wiring + restart banner + port rebind sync -- IpcError ResinUpstream: wire to remaining commands (P5 leftover) -- Restart banner: sidecar-status event add "restarting" payload -- Port rebind sync: after restart, re-fetch port list, sync port_mappings -- Test: cargo test IpcError variants + vitest restart banner - -### T6-7: Network diagnostics GUI panel -- New view or Settings sub-section "Diagnostics" -- Shows: sidecar PID, API port, healthz last check, IPC latency, request log tail -- Test: vitest diagnostics panel renders + data refresh - -### T6-8: Closed-loop tests per capability -- cargo + vitest per item + i18n check + tsc green - -### T6-9: Build + smoke + sync -- pnpm build -> cargo build --release -> grep chunk hash -> smoke -> codegraph sync -> push - -## Constraints -- Ponytail full: smallest diff, root cause not symptom, deletion over addition -- Every platform (cargo + vitest) must have test closed-loop -- pwm pro gpt56 for technical detail research (avoid hallucination) -- No fork Resin — all shell-side -- i18n: 18 locales, all new keys -- AGENTS.md sections 5/6/7: build + push + CRLF per commit diff --git a/docs/history/phases/GRILL_T6_PLAN.md b/docs/history/phases/GRILL_T6_PLAN.md deleted file mode 100644 index 0693c6ce..00000000 --- a/docs/history/phases/GRILL_T6_PLAN.md +++ /dev/null @@ -1,139 +0,0 @@ -> **Superceded by `docs/GRILL_T6_NETWORK_LAYER_PLAN.md`** (2026-08-12). This earlier plan defined T6 (Q1-Q6) as an architecture-cleanup + unified-bugfix round and is preserved as a historical audit trail only. The live T6 work was re-executed under the Network Layer plan, which re-organized the same Q1-Q5 caps into a clean T6-1 through T6-9 schedule with closed-loop test gates and a handoff doc. Do NOT cite this file as the current plan; cite `docs/GRILL_T6_NETWORK_LAYER_PLAN.md`. - -# GRILL T6 PLAN — Architecture Cleanup + Unified Bug Fix - -> **Date**: 2026-08-10 -> **Grill round**: T6 (Q1-Q6 all resolved, handoff authorized) -> **Supersedes**: T5 trace_id + IpcError refactoring (P29/P30) -> **Ponytail**: shortest working diff, reuse existing helpers, no new deps - ---- - -## Executive Summary - -Grill T6 identified 6 bugs from user's live testing of `release/windows-gui/EgressAPIKEY.exe`. -The previous agent's handoff claimed "all resolved" — **code-level audit proved 5 of 6 were NOT landed**. -This document is the consolidated plan table for the execution phase. - ---- - -## Bug → Root Cause → Fix → Test Mapping - -### Bug 1: HTTP port shows "unreachable" despite being alive - -| Field | Value | -|-------|-------| -| **User evidence** | A6: "1791端口 HTTP状态已开放且正常工作,验证结果收到404 Not Found,证明该端口运行HTTP协议服务。GUI依旧显示端口不可达" | -| **Root cause** | `port_health_check` at `mod.rs:1820` sends `CONNECT 127.0.0.1:{port} HTTP/1.1` — Resin's HTTP proxy doesn't support CONNECT tunneling, returns 404/error → code marks unreachable | -| **Fix** | Change HTTP probe from `CONNECT` to `GET / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n\r\n` — any HTTP response (200/404/400) proves the port is alive | -| **Location** | `src-tauri/src/commands/mod.rs` line ~1820 | -| **Test** | cargo unit test: mock TCP listener returns `HTTP/1.1 404 Not Found\r\n\r\n` + assert `reachable: true, socks5_ok: false, protocol_mismatch: false` | - -### Bug 2: Strategy "unknown variant `balanced`" error - -| Field | Value | -|-------|-------| -| **User evidence** | "平台内什么策略我都设置不了,报错:strategy config invalid: unknown variant `balanced`" | -| **Root cause** | `src/lib/policy.ts` still exists with a 3-branch switch using Resin native values (`BALANCED`/`PREFER_LOW_LATENCY`/`PREFER_IDLE_IP`). `ALLOCATION_POLICIES` in `ipc.ts:84` has Resin-native values. `PlatformsView.tsx:434` displays via `policyToI18nKey(pol)` from the old `policy.ts`. The shell 6-option `StrategyId` (random/sequential/latency/quality/bandwidth/protocol_weight) was committed in i18n + Rust but **never wired into the GUI select**. The `<select>` sends `BALANCED` as the value, but the Rust `strategy_config_put` command validates against the shell 6-option enum, rejecting `balanced` (lowercase, from i18n label being confused with the value). | -| **Fix** | (a) Delete `src/lib/policy.ts` — replace `policyToI18nKey` with `strategyToI18nKey` using the 6 committed `strategy.*` i18n keys. (b) Replace `ALLOCATION_POLICIES` in `ipc.ts` with `STRATEGY_IDS = ["random","sequential","latency","quality","bandwidth","protocol_weight"]`. (c) New `strategyToResinPolicy(): StrategyId → AllocationPolicy`: random/sequential→B+ALANCED, latency→PREFER_LOW_LATENCY, quality→PREFER_IDLE_IP, bandwidth→BALANCED, protocol_weight→BALANCED. (d) `ipcPlatformUpdate` translates `StrategyId → AllocationPolicy` before PATCH. (e) `PlatformsView` and `TopologyView` use `strategyToI18nKey` + `STRATEGY_IDS`. | -| **Location** | `src/lib/policy.ts` (DELETE), `src/lib/ipc.ts` L84-85, L163-171, `src/views/PlatformsView.tsx` L433-434, `src/views/TopologyView.tsx` L15, L508 | -| **Test** | vitest: (1) `strategyToResinPolicy` maps all 6 values correctly; (2) `strategyToI18nKey` maps all 6 values to existing i18n keys; (3) `ipcPlatformUpdate` translates before forwarding; (4) invalid strategy value rejected at TS boundary | - -### Bug 3: Subscription drag-to-reorder doesn't work - -| Field | Value | -|-------|-------| -| **User evidence** | A5: "只有爬取的鼠标,具体表现拖动完全没反应" | -| **Root cause** | `SubscriptionsView.tsx:267` `setPointerCapture(e.pointerId)` locks ALL pointer events onto the source `<li>` element. After capture, `onPointerEnter` on OTHER rows **never fires** because the captured element intercepts all pointer events. The user sees the cursor move but nothing happens — the list doesn't reorder. | -| **Fix** | Delete the `setPointerCapture` line. Without capture, pointer events bubble naturally — `onPointerEnter` fires on every row the cursor passes over. The window-level `pointerup` listener (already implemented L256) handles release-outside-row. | -| **Location** | `src/views/SubscriptionsView.tsx` line 267 — delete the single line | -| **Test** | vitest: simulate pointer down on row 0, pointer enter on row 2, pointer up → assert list reordered (row 0 content now at index 2) | - -### Bug 4: Port conflict error shows `[object Object]` - -| Field | Value | -|-------|-------| -| **User evidence** | A6: "端口17111添加失败" + "报错的是[object Object]" | -| **Root cause** | **Two bugs**: (1) `map_resin_error` at `mod.rs:83` matches `CONFLICT` (from HTTP 409 Conflict status) BEFORE the `bind` match at L97 → port bind error gets `error.conflict` instead of `error.bindConflict:PORT`. (2) `PlatformsView.tsx:433` `.catch((err) => ... String(err))` — `String(IpcError)` returns `[object Object]` because IpcError is a complex object, not an Error instance. `translateError` exists (L5 import) but is NOT used at L433. | -| **Fix** | (1) Reorder `map_resin_error`: move `bind` match (L97-102) BEFORE `CONFLICT` match (L83-84). (2) Change `PlatformsView.tsx:433` catch to `translateError(e, t)`. | -| **Location** | `src-tauri/src/commands/mod.rs` L83/L97 reorder, `src/views/PlatformsView.tsx` L433 | -| **Test** | cargo: reorder test — `map_resin_error("409 Conflict: listen on port 17111: bind: Only one usage...")` → `error.bindConflict:17111`. vitest: L433 catch path uses translateError. | - -### Bug 5: Port auth info username missing platform prefix - -| Field | Value | -|-------|-------| -| **User evidence** | A6: "SOCKS5账号命名格式必须为<平台名称>.<账号名称>...正确账号为Default.port-1792...但GUI返回port-1792" | -| **Root cause** | `port_auth_info` at `mod.rs:1735-1741` returns `username = mapping.account` (or `port-{port}` fallback). But Resin SOCKS5 requires `<Platform>.<Account>` format. Without the platform prefix, SOCKS5 auth succeeds (password = proxy_token validates) but CONNECT returns General failure (error 1) because Resin doesn't know which platform the traffic belongs to. | -| **Fix** | Change username to `format!("{}.{}", mapping.platform_name, mapping.account)` (or `format!("{}.port-{}", mapping.platform_name, port)` for empty-account fallback). | -| **Location** | `src-tauri/src/commands/mod.rs` L1735-1741 | -| **Test** | cargo: mock DbPool with a `PortMapping { port: 1792, platform_name: "Default", account: "port-1792" }` → assert `username == "Default.port-1792"` | - -### Bug 6: Two headless.exe binaries - -| Field | Value | -|-------|-------| -| **User evidence** | "egressapikey_headless.exe和egressapikey-headless.exe,怎么有两个" | -| **Root cause** | `build-all.ps1` filename pattern mismatch — one from cargo `--bin` flag (`egressapikey_headless.exe` with underscore) and one from a rename step (`egressapikey-headless.exe` with hyphen) | -| **Fix** | Already partially addressed in P30-AUDIT commit (ec1f629). Verify only one exe is staged in `release/windows-gui/`. | -| **Test** | Verify `release/windows-gui/` has exactly one headless binary via build script assertion | - ---- - -## Execution Phases (small-step commits, each with closed-loop test) - -### Phase 1: Strategy unification (Bug 2 + Bug 4 strategy prefix) -1. Delete `src/lib/policy.ts` -2. Create `strategy.ts` with `StrategyId`, `STRATEGY_IDS`, `strategyToI18nKey()`, `strategyToResinPolicy()` -3. Update `src/lib/ipc.ts`: replace `ALLOCATION_POLICIES` with `STRATEGY_IDS`, add translation in `ipcPlatformUpdate` -4. Update `src/views/PlatformsView.tsx` L433-434: use `STRATEGY_IDS` + `strategyToI18nKey` -5. Update `src/views/TopologyView.tsx` L15/L508: replace `policyToI18nKey` import + call -6. **Test**: vitest — mapping + option round-trip + invalid rejection -7. **Commit**: `P31-T6: strategy unification — shell 6-option as sole UI source of truth` - -### Phase 2: Error handling unification (Bug 4) -1. `src-tauri/src/commands/mod.rs`: reorder `map_resin_error` match arms (bind before CONFLICT) -2. `src/views/PlatformsView.tsx` L433: change `.catch` to use `translateError(e, t)` -3. Scan ALL views for remaining `String(err)` patterns → replace with `translateError` -4. **Test**: cargo — bind-conflict maps to `error.bindConflict:PORT`; vitest — translateError on IpcError -5. **Commit**: `P31-T6: error handling unification — bind conflict before CONFLICT + translateError everywhere` - -### Phase 3: Drag fix (Bug 3) -1. `src/views/SubscriptionsView.tsx` L267: delete `setPointerCapture` line -2. Add `releasePointerCapture` in `onPointerUpRow` as safety net -3. **Test**: vitest — simulate drag reorder -4. **Commit**: `P31-T6: drag fix — remove setPointerCapture blocking onPointerEnter` - -### Phase 4: Port health + auth info fix (Bug 1 + Bug 5) -1. `src-tauri/src/commands/mod.rs` L1820: change HTTP greeting from `CONNECT` to `GET / HTTP/1.1` -2. `src-tauri/src/commands/mod.rs` L1735-1741: change `port_auth_info` username to `{platform}.{account}` -3. **Test**: cargo — HTTP probe sends GET; port_auth_info returns platform-prefixed username -4. **Commit**: `P31-T6: port health HTTP GET + SOCKS5 Platform.Account username format` - -### Phase 5: Build + stage + smoke (AGENTS §5) -1. `pnpm build` + `cargo build --release -p egressapikey-app --features custom-protocol` -2. Stage to `release/windows-gui/EgressAPIKEY.exe` -3. Verify Vite chunk hash embedded in exe bytes -4. Smoke launch: MainWindowTitle + resin.exe child alive -5. **Commit**: `P31-T6: build + stage release exe` - ---- - -## ADRs - -| ADR | Title | Status | -|-----|-------|--------| -| ADR-0030 | Strategy name unification — shell 6-option as sole UI source of truth | PROPOSED | -| ADR-0031 | Drag fix — remove setPointerCapture (Pointer Events capture blocks onPointerEnter) | PROPOSED | -| ADR-0032 | Port health probe — HTTP GET instead of CONNECT + port_auth_info Platform.Account username | PROPOSED | - ---- - -## Constraints - -- **Ponytail full**: shortest working diff, reuse existing helpers, no new deps -- **ctx_* first**: file edits via `ctx_execute_file` + Node `fs.writeFileSync` -- **Single-threaded**: no subagents -- **i18n**: 18 base locales must stay in sync (strategy.* keys already exist from T5) -- **Closed-loop tests**: every fix must have a test that would fail if the fix is reverted -- **AGENTS §5**: each commit touching src/crates must rebuild + stage release exe + verify Vite hash diff --git a/docs/history/phases/GRILL_T7_DIAGNOSTICS_PLAN.md b/docs/history/phases/GRILL_T7_DIAGNOSTICS_PLAN.md deleted file mode 100644 index 21d9618c..00000000 --- a/docs/history/phases/GRILL_T7_DIAGNOSTICS_PLAN.md +++ /dev/null @@ -1,180 +0,0 @@ -# GRILL T7: Diagnostics Page Refactor — Grill Decisions & Execution Plan - -Date: 2026-08-13 -Status: GRILL COMPLETE — awaiting user "start building" instruction -Grill round: T7 (Q1-Q7) - -## Problem Statement - -Three bugs in the current Settings > Diagnostics panel: - -1. **ps1 console window flash**: `check_firewall_status` IPC uses `std::process::Command::new("powershell")` without `CREATE_NO_WINDOW` flag (0x08000000), causing a PowerShell console window to flash on every call. -2. **Diagnostics panel cramped in Settings**: The diagnostic SectionCard in SettingsView.tsx (L428+) crams sidecar port/mode, firewall status, request log table, and refresh button into one card — not intuitive for a diagnostics/debug surface. -3. **Subprocess deadlock + window respawn**: `Command::new("powershell").output()` is synchronous with no timeout — can permanently block if PowerShell profile loads slowly or AV intervenes. SettingsView mount auto-triggers `refreshDiagnostics()` so every visit to Settings spawns the PS window again. - -## Root Cause (code-level verified via ctx_batch_execute) - -- `src-tauri/src/commands/mod.rs:1939` — `Command::new("powershell")` without `creation_flags(0x08000000)` -- `src-tauri/src/commands/mod.rs:1942` — `.output()` synchronous, no timeout wrapper -- `src/views/SettingsView.tsx:145` — mount auto-calls `refreshDiagnostics()` → triggers firewall IPC -- `src/views/SettingsView.tsx:428` — diagnostics SectionCard inside SettingsView - -## Grill Decisions Summary - -| Q | Topic | Decision | New code? | Test shape | -|---|-------|----------|-----------|------------| -| Q1 | Fix scope | A: unified fix in one commit | Yes | per-phase | -| Q2 | DiagnosticsView content | B: full diagnostics hub, sole entry point, no redundancy | ~200 TSX | vitest closed-loop | -| Q3 | Request log refresh | A: configurable polling (default 5s) + manual refresh + visibilitychange pause | ~30 TSX | vitest interval test | -| Q4 | Poll interval storage | A: `settings.json#diagPollInterval` via tauri-plugin-store | ~20 TS | vitest load/save | -| Q5 | Cross-platform firewall | A: Windows实测 + Linux(systemctl/firewalld/ufw) + macOS(pfconf/pfctl) — all with 5s timeout + CREATE_NO_WINDOW | ~80 Rust | cargo timeout test | -| Q6 | Nav icon/key | A: `diagnostics` key + `Stethoscope` lucide icon, position 6 (before settings) | ~5 TSX | vitest nav render | -| Q7 | SettingsView cleanup | A: delete diagnostics SectionCard, keep About port/mode overview, dynamically handle dependencies | ~-100 TSX | vitest no-render test | - -## pwm pro Research Results - -### Subprocess no-window + timeout (gpt56_sol) -Template: `tokio::process::Command` + `creation_flags(0x08000000)` on Windows + `tokio::time::timeout(Duration::from_secs(5), child.wait_with_output())`. Cross-platform: Linux/macOS use `sh -c`. On timeout: `child.kill().await` + return error. - -### Non-root firewall detection (sonar) -- **Windows**: `Get-NetFirewallProfile` works as non-admin -- **Linux**: `ufw status` and `iptables -L` require root. `systemctl is-active ufw` / `systemctl is-active firewalld` may work as non-root (distro-dependent). Reading `/proc/net/ip_tables_names` may work but is not definitive. -- **macOS**: `pfctl -s info` requires root. Checking `/etc/pf.conf` existence is coarse but non-root safe. -- All non-Windows: best-effort, return descriptive error if permission denied. - -## Execution Plan (dependency graph) - -### T7-1: Fix check_firewall_status (Rust backend) -- Replace `std::process::Command` with `tokio::process::Command` -- Add `creation_flags(0x08000000)` on Windows (CREATE_NO_WINDOW) -- Add `tokio::time::timeout(5s)` wrapper -- Linux: try `systemctl is-active ufw` then `systemctl is-active firewalld` then read `/proc/net/ip_tables_names` then fallback -- macOS: try `pfctl -s info` then check `/etc/pf.conf` existence then fallback -- On timeout: kill child, return `{firewall_on: false, detail: "Detection timed out"}` -- **Acceptance**: 1 cargo test (timeout behavior with mock slow command) -- **Estimate**: ~80 lines Rust - -### T7-2: Create DiagnosticsView.tsx (frontend) -- New file: `src/views/DiagnosticsView.tsx` -- Sections: - - Sidecar status card: port, mode, PID, healthz last check, IPC latency (from `ipcGetSidecarStatus`) - - Firewall status card (from `ipcCheckFirewallStatus`) - - Request log table card (from `ipcRequestLogTail`) with auto-refresh polling - - Exit IP probe card (from `ipcProbeExitIp`) — port selector + protocol selector + probe button - - Port health check card (from `ipcPortHealthCheck`) — port input + protocol + check button - - Log directory button (from `get_log_dir` then `openPath`) - - Sidecar log buffer card (from `ipcGetSidecarLogs`) — scrollable ring buffer display -- Polling: `setInterval` with configurable interval (default 5000ms), `visibilitychange` pause/resume -- Manual refresh button alongside auto-polling -- **Acceptance**: 3 vitest (renders all cards, poll interval from settings, manual refresh triggers IPC) -- **Estimate**: ~200 lines TSX - -### T7-3: Add nav tab in App.tsx -- Add `Stethoscope` to lucide-react imports -- Add `{ key: "diagnostics", icon: Stethoscope }` to NAV_ITEMS at position 6 (before settings) -- Add `{view === "diagnostics" && <DiagnosticsView />}` to main content -- Add i18n key `nav.diagnostics` across all 18 locales -- **Acceptance**: 1 vitest (nav renders diagnostics tab) -- **Estimate**: ~10 lines TSX - -### T7-4: SettingsView cleanup -- Delete from SettingsView.tsx: - - Diagnostics SectionCard (L428+) - - `firewallStatus` state + `setFirewallStatus` - - `reqLogs` state + `setReqLogs` - - `diagBusy` state + `setDiagBusy` - - `refreshDiagnostics` function - - `void refreshDiagnostics()` call in mount effect - - Imports: `ipcCheckFirewallStatus`, `ipcRequestLogTail`, `FirewallStatus`, `RequestLogEntry` -- Keep in SettingsView.tsx: - - `sidecarStatus` state + `ipcGetSidecarStatus` (for About section port/mode display) - - IPC commands stay registered in main.rs (DiagnosticsView calls them) -- Dynamic dependency check: if any test references removed state, adjust the test -- **Acceptance**: 2 vitest (diagnostics card NOT rendered in SettingsView, About port/mode still rendered) -- **Estimate**: ~-100 lines TSX - -### T7-5: Poll interval config (settings.json) -- Add `loadDiagPollInterval` / `saveDiagPollInterval` to `src/lib/settings.ts` -- Read on DiagnosticsView mount, save on change -- Default 5000ms, range 1000-60000, clamp to range -- UI: number input or dropdown (1s/2s/5s/10s/30s/60s) in DiagnosticsView header -- **Acceptance**: 1 vitest (load + save round-trip) -- **Estimate**: ~30 lines TS - -### T7-6: i18n keys for DiagnosticsView -- New keys across all 18 locales: - - `nav.diagnostics` - - `diagnostics.title` - - `diagnostics.sidecarStatus` - - `diagnostics.firewall` - - `diagnostics.reqLog` - - `diagnostics.exitIpProbe` - - `diagnostics.portHealth` - - `diagnostics.logs` - - `diagnostics.pollInterval` - - `diagnostics.openLogDir` - - `diagnostics.sidecarLogs` - - `diagnostics.refresh` - - `diagnostics.noLogs` -- **Acceptance**: `pnpm i18n:check` green -- **Estimate**: ~13 keys x 18 locales - -### T7-7: Closed-loop tests -- Update existing `SettingsView.test.tsx`: remove diagnostics panel tests (moved to DiagnosticsView) -- New `DiagnosticsView.test.tsx`: - - Renders sidecar status card with port + mode + PID + healthz + latency - - Renders firewall status card - - Renders request log table with entries from IPC - - Poll interval loaded from settings - - Manual refresh button triggers IPC - - Exit IP probe button works - - Port health check button works -- `ipc.test.ts`: existing firewall/requestLog/sidecarStatus/probe/healthCheck wrappers already tested -- **Acceptance**: all tests green -- **Estimate**: ~6 vitest - -### T7-8: Build + smoke + codegraph sync + push -- `pnpm build` (tsc + vite) -- `cargo build --release -p egressapikey-app --features custom-protocol` -- Stage exe to `release/windows-gui/EgressAPIKEY.exe` -- Grep new Vite chunk hash inside exe bytes -- Smoke launch: MainWindowTitle=EgressAPIKEY, no ps1 window flash -- `codegraph sync .` -- `git push` -- **Acceptance**: build green, chunk hash confirmed, no console flash, tests green - -## Dependency Graph - -T7-1 (Rust fix) and T7-2 (DiagnosticsView) can start in parallel. -T7-3 depends on T7-2. -T7-4 depends on T7-2. -T7-5 depends on T7-2. -T7-6 depends on T7-2. -T7-7 depends on T7-2, T7-3, T7-4, T7-5, T7-6. -T7-8 depends on all. - -## Estimates - -| Phase | Lines | New files | Tests | -|-------|-------|-----------|-------| -| T7-1 | ~80 Rust | 0 | 1 cargo | -| T7-2 | ~200 TSX | 1 (DiagnosticsView.tsx) | 0 (tests in T7-7) | -| T7-3 | ~10 TSX | 0 | 0 | -| T7-4 | ~-100 TSX | 0 | 0 | -| T7-5 | ~30 TS | 0 | 0 | -| T7-6 | ~234 i18n entries | 0 | 0 | -| T7-7 | ~120 TSX | 1 (DiagnosticsView.test.tsx) | 6 vitest + test updates | -| T7-8 | 0 | 0 | 0 | -| **Total** | ~250 net new | 2 | 7+ | - -## ADR Consideration - -ADR-0029: Diagnostics as a First-Class View (separation from Settings) -- Hard to reverse: yes (once diagnostics is its own nav tab, going back to Settings is a regression) -- Surprising without context: no (standard desktop app pattern) -- Real trade-off: yes (took the decision to split vs keep inline) -- Create ADR-0029 - -## Handoff Prompt - -After user says "start building", execute the T7-1 through T7-8 phases in dependency order. \ No newline at end of file diff --git a/docs/history/phases/GRILL_T8_EXIT_IP_STRATEGY_PLAN.md b/docs/history/phases/GRILL_T8_EXIT_IP_STRATEGY_PLAN.md deleted file mode 100644 index a6e8ccd1..00000000 --- a/docs/history/phases/GRILL_T8_EXIT_IP_STRATEGY_PLAN.md +++ /dev/null @@ -1,196 +0,0 @@ -# GRILL T8 EXIT_IP_STRATEGY PLAN - -> Branch: codex/rust-port (parallel fix branch) -> Date: 2026-08-13 -> Status: PLANNED (all Q1-Q5 decisions confirmed) -> Budget: 100,000,000 tokens -> Skills: ponytail:full, grill-with-docs, domain-modeling - -## Decision Summary - -| Q | Topic | Decision | Reference | -|---|-------|----------|-----------| -| Q1 | Auto-reconnect fallback | A — Expose Resin passive_circuit_breaker + max_consecutive_failures to GUI whitebox | Resin control_plane_platform.go:34, system config handler | -| Q2 | Strategy effect verification | A+C — strategy_verify IPC + DiagnosticsView panel | New IPC + extend DiagnosticsView | -| Q3 | Domain stickiness | A — Change sticky_ttl default from 168h to 0s + add GUI explanation | ~10 lines, strategy.ts + PlatformsView.tsx | -| Q4 | Subscription update interval | A — Add update_interval param to subscription_add, GUI input (default 30s, 1s-3600s) | commands/mod.rs:536 + SubscriptionsView.tsx | -| Q5 | Graceful strategy switch | C — Both "close all connections" and "reset kernel" via kill+restart sidecar | No Resin close-all API; shell-side boot_resin reuse | - -## Architecture Facts (from Resin source code) - -1. **passive_circuit_breaker_disabled** — platform-level boolean field - - PATCH /api/v1/platforms/{id} with {"passive_circuit_breaker_disabled": false} - - Default: false (circuit breaker ENABLED) - - When enabled: nodes with consecutive failures get auto-isolated - -2. **max_consecutive_failures** — system-level integer (default 7) - - GET /api/v1/system/config → read current value - - PATCH /api/v1/system/config with {"max_consecutive_failures": N} - - Controls how many consecutive failures before a node is isolated - -3. **sticky_ttl** — platform-level duration (stored as nanoseconds: sticky_ttl_ns) - - Default in shell: 168h0m0s (7 days) ← NEEDS CHANGE to 0s - - PATCH /api/v1/platforms/{id} with {"sticky_ttl": "0s"} - - 0s = no stickiness (every request can use a different exit IP) - - clash-verge-rev equivalent: session-sticky with configurable TTL - -4. **update_interval** — subscription-level duration string - - POST /api/v1/subscriptions with {"update_interval": "30s"} - - Currently hardcoded to "30s" in commands/mod.rs:536 - - PATCH /api/v1/subscriptions does NOT exist (v1.2.0) - - To change interval: delete + recreate (mesh with Q4-A) - -5. **No close-all-connections API** — Resin v1.2.0 has no public endpoint - - server.go:179 has graceful Shutdown() but no "close all" handler - - No EvictAll/DeleteLease/PurgeLease in internal handlers - - Shell-side: kill resin.exe + boot_resin() = equivalent to close-all + reset-kernel - -## Execution Plan (ordered by dependency graph) - -### Phase T8-1: Expose circuit breaker config (Q1-A) - -**Files:** -- `crates/resin-core/src/resin_client.rs` — add system_config_get/system_config_patch methods (GET/PATCH /api/v1/system/config) -- `src-tauri/src/commands/mod.rs` — add system_config_get/system_config_patch IPC commands; extend platform_update to accept passive_circuit_breaker_disabled -- `src/lib/ipc.ts` — add ipcSystemConfigGet/ipcSystemConfigPatch wrappers; extend ipcPlatformUpdate with circuitBreakerDisabled param -- `src/views/SettingsView.tsx` — add "Circuit Breaker" section: max_consecutive_failures input + enable/disable toggle -- `src/views/PlatformsView.tsx` — per-platform circuit breaker toggle in platform card - -**Tests:** -- cargo: mockito system_config_get round-trip, system_config_patch round-trip, platform_update with passive_circuit_breaker_disabled field -- vitest: ipcSystemConfigGet wrapper forward, ipcSystemConfigPatch wrapper validation, platform card displays circuit breaker state - -**Acceptance:** -- GUI Settings shows "Circuit Breaker" section with max_consecutive_failures (default 7) -- Each platform card has a toggle for circuit breaker enable/disable -- PATCH /api/v1/platforms/{id} with passive_circuit_breaker_disabled works -- PATCH /api/v1/system/config with max_consecutive_failures works -- cargo test green, vitest green - -### Phase T8-2: Strategy effect verification IPC (Q2-A) - -**Files:** -- `src-tauri/src/commands/mod.rs` — add strategy_verify IPC: takes platform_name + sample_count (N), sends N probe requests through the port, collects exit IP + latency for each, returns distribution stats -- Reuses existing port_health_check TCP probe + Resin /metrics/realtime/leases for IP attribution -- `src/lib/ipc.ts` — add ipcStrategyVerify wrapper - -**Tests:** -- cargo: strategy_verify with mock Resin (returns distribution stats), N=3 sample, verifies struct shape -- vitest: ipcStrategyVerify wrapper forward + validation (N range 3-50) - -**Acceptance:** -- strategy_verify("Default", 10) returns { samples: [{ip, latency_ms}], distribution: {ip1: 4, ip2: 6}, avg_latency: 120, strategy: "random" } -- cargo test green, vitest green - -### Phase T8-3: Strategy verification DiagnosticsView panel (Q2-C) - -**Files:** -- `src/views/DiagnosticsView.tsx` — add "Strategy Verification" card: platform selector dropdown, N input (default 10), run button, result display (IP distribution pie/bar + latency histogram + strategy name) -- Uses chart.js or simple CSS bars (Ponytail: prefer SVG/CSS bars, no new dep) - -**Tests:** -- vitest: DiagnosticsView renders strategy panel, run button calls ipcStrategyVerify, displays distribution - -**Acceptance:** -- DiagnosticsView has "Strategy Verification" section -- User selects platform, clicks Run, sees IP distribution + avg latency -- vitest green - -### Phase T8-4: Fix sticky_ttl default to 0s (Q3-A) - -**Files:** -- `src/lib/strategy.ts` — no change needed (strategy mapping untouched) -- `src/views/PlatformsView.tsx` — change default stickyTtl from "168h0m0s" to "0s", add tooltip/label: "0 = no stickiness (every request may use different exit IP)" -- `src/lib/ipc.ts` — ipcPlatformCreateWithFields: default stickyTtl to "0s" if not specified -- `src-tauri/src/commands/mod.rs` — platform_create_with_fields: default sticky_ttl to "0s" in the JSON body sent to Resin -- i18n: add strategy.stickyTtlHint key to all 18 locales - -**Tests:** -- vitest: PlatformsView shows sticky_ttl=0s by default, tooltip text present -- cargo: platform_create_with_fields sends sticky_ttl="0s" when not specified - -**Acceptance:** -- New platforms have sticky_ttl = 0s by default -- GUI shows explanation text next to sticky_ttl input -- Existing platforms with 168h keep their value (only default changes) -- i18n:check green (287+1 keys × 18 locales) - -### Phase T8-5: Subscription update_interval whitebox (Q4-A) - -**Files:** -- `src-tauri/src/commands/mod.rs` — subscription_add: add update_interval: String param (default "30s"), validate format (Go duration string, 1s-3600s range), pass into POST body -- `src/views/SubscriptionsView.tsx` — add update_interval input field (default "30s", placeholder "30s") next to URL input -- `src/lib/ipc.ts` — extend ipcSubscriptionAdd with updateInterval param -- i18n: add subscription.updateInterval key to all 18 locales - -**Tests:** -- cargo: subscription_add with custom update_interval "60s" posts correct body -- vitest: SubscriptionsView form has update_interval field, submits correct value - -**Acceptance:** -- GUI subscription form has "Update Interval" input (default 30s) -- Creating a subscription with "60s" results in Resin scheduler fetching every 60s -- i18n:check green - -### Phase T8-6: Close all connections + Reset kernel (Q5-C) - -**Files:** -- `src-tauri/src/commands/mod.rs` — add two IPC commands: - - close_all_connections: kill resin.exe child + boot_resin() (same process restart, ~2s downtime) - - reset_kernel: same implementation (kill + boot_resin), but different semantic label + log message - - Both reuse existing sidecar.rs boot_resin infrastructure - - Add tracing::info!("user-initiated close-all-connections") / "user-initiated kernel-reset" -- `src/lib/ipc.ts` — add ipcCloseAllConnections/ipcResetKernel wrappers -- `src/views/DiagnosticsView.tsx` — add two buttons in a "Connection Control" card: "Close All Connections" (red) + "Reset Kernel" (orange), each with confirm dialog -- i18n: add diagnostics.closeAllConnections + diagnostics.resetKernel keys to all 18 locales - -**Tests:** -- cargo: close_all_connections command validates (no crash on missing sidecar, returns IpcError) -- vitest: DiagnosticsView renders both buttons, click triggers correct IPC call - -**Acceptance:** -- DiagnosticsView has "Connection Control" card with two buttons -- Clicking "Close All Connections" kills and restarts resin.exe -- Clicking "Reset Kernel" kills and restarts resin.exe (same impl, different label) -- Existing SSE connections are dropped (expected — this is the point) -- tray icon shows restart status -- i18n:check green - -### Phase T8-7: Full gate + build + push - -1. cargo test -p resin-core --lib (expect all green) -2. cargo build --release -p egressapikey-app --features custom-protocol -3. pnpm build (tsc + vite, new chunk hash) -4. grep new Vite chunk hash in exe bytes -5. pnpm test (vitest all green) -6. pnpm i18n:check -7. codegraph sync . -8. git add + commit + push - -## Dependency Graph - -``` -T8-1 (circuit breaker) ──┐ - ├──> T8-7 (gate + build + push) -T8-2 (strategy verify) ──┤ - │ -T8-3 (diag panel) ────────┤ - │ -T8-4 (sticky_ttl) ────────┤ - │ -T8-5 (sub interval) ─────┤ - │ -T8-6 (close-all/reset) ──┘ -``` - -T8-1 through T8-6 are independent of each other (different files/features), but commits are sequential (Ponytail: small step commits). T8-7 depends on all. - -## Resin API surface used (all verified from source) - -| Endpoint | Method | Used in | Field | -|----------|--------|---------|-------| -| /api/v1/platforms/{id} | PATCH | T8-1, T8-4 | passive_circuit_breaker_disabled, sticky_ttl | -| /api/v1/system/config | GET | T8-1 | max_consecutive_failures | -| /api/v1/system/config | PATCH | T8-1 | max_consecutive_failures | -| /api/v1/subscriptions | POST | T8-5 | update_interval | -| (no close-all API) | — | T8-6 | shell-side kill+restart | diff --git a/docs/history/phases/GRILL_T8_PLATFORM_PAGE_FIX.md b/docs/history/phases/GRILL_T8_PLATFORM_PAGE_FIX.md deleted file mode 100644 index 61e924c4..00000000 --- a/docs/history/phases/GRILL_T8_PLATFORM_PAGE_FIX.md +++ /dev/null @@ -1,50 +0,0 @@ -# GRILL T8 — Platform Page UX Fix (2026-08-13) - -> Branch: codex/rust-pass | Supersedes: T7 | Depends on: T5 (trace_id + IpcError), T6 (network layer) - -## Q&A Decision Summary - -| Q | Topic | Decision | ADR | -|---|---|---|---| -| Q1 | Drag port to platform silently flips auth_required to true | B (new IPC port_bind_platform, not port_upsert) | ADR-0029 | -| Q2 | Drag feedback: text selection + poor visual | A (3-layer: preventDefault + user-select:none + ghost/ring/scale) | ADR-0029 | -| Q3 | A-class strategy hidden behind chevron expand | A (inline badge/chip on card, delete expand panel) | ADR-0029 | -| Q4 | A-class selectors: manual has no IP picker, region/sub are text boxes | A (chip-based multi-select, reuse ipcNodeList + ipcSubscriptionList) | ADR-0029 | -| Q5 | New port auto-assigned to Default platform | A (empty string = unbound, no default platform) | ADR-0029 | - -## T8 Execution Plan - -| Step | Task | Files | ADR | Test | -|------|------|------|-----|------| -| T8-1 | New IPC port_bind_platform(port, platform_name) — only PATCH endpoint platform binding, no auth_required touch | src-tauri/src/commands/mod.rs, crates/resin-core/src/resin_client.rs | 0029 | 2 cargo mockito (PATCH platform_name only), 1 vitest wrapper forward | -| T8-2 | bindPortToPlatform TS calls ipcPortBindPlatform instead of ipcPortUpsert | src/views/PlatformsView.tsx | 0029 | 1 vitest (bind calls port_bind_platform not port_upsert) | -| T8-3 | Drag 3-layer visual: (1) onPointerDown preventDefault + body userSelect:none (2) source opacity-50 + cursor-grabbing (3) target ring-2 ring-offset-2 scale-[1.02] transition | src/views/PlatformsView.tsx | 0029 | 2 vitest (preventDefault + userSelect toggled) | -| T8-4 | A-class inline badge row on platform card: colored chips showing A:manual / A:region[HK,JP] / A:subscription[alpha] / A:quality top-N | src/views/PlatformsView.tsx | 0029 | 2 vitest (badge renders per type + shows values) | -| T8-5 | Delete expandedPlatform state + chevron + strategy-inline panel; A-class edit via clicking badge popover | src/views/PlatformsView.tsx | 0029 | 1 vitest (no expand/collapse in DOM, badge click opens edit) | -| T8-6 | Chip-based multi-select for A-class: (a) manual= nodeList checkbox chips (b) region= distinct regions toggle chips (c) subscription= subscriptionList toggle chips (d) quality= top_n input + live preview top-3 | src/views/PlatformsView.tsx | 0029 | 4 vitest (one per a_class type, chips render + toggle) | -| T8-7 | handleAddPort: platform_name = newPlatformName.trim() || empty string (unbound); port_upsert Rust accepts empty (skip port_mappings row); UI shows Unbound badge | src/views/PlatformsView.tsx, src-tauri/src/commands/mod.rs | 0029 | 2 vitest (empty creates unbound + badge shows) | -| T8-8 | i18n keys for new UI strings across 18 locales | src/locales/*/platform.json | 0029 | i18n:check green | -| T8-9 | Build exe + stage + smoke + codegraph sync + push | scripts/build-all.ps1 | - | release exe smoke | - -Dependency: T8-1 to T8-2, then (T8-3 || T8-4 || T8-6 || T8-7) parallel, then T8-5, then T8-8, then T8-9 - -## Root cause analysis (code-level) - -### Bug 1 (auth flip on drag) -bindPortToPlatform L258 calls ipcPortUpsert without auth_required. Rust port_upsert L1572-1600 defaults auth_required = true. Fix: new port_bind_platform IPC only PATCHes platform_name, never touches auth. - -### Bug 2 (text selection + poor feedback) -onPointerDown L360 only calls setDraggingPort, no e.preventDefault(). Browser triggers text selection. Only opacity-60 + ring-2 as feedback. Fix: preventDefault + userSelect:none + cursor-grabbing + target scale/ring/offset. - -### Bug 3 (A-class hidden) -Strategy panel defaults to expandedPlatform = null (collapsed). User must click chevron to see A-class. Fix: inline badge row on card, delete expand panel. - -### Bug 4 (selectors are text boxes) -manual: no IP picker. region: text input. subscription: text input. Fix: chip-based multi-select from live nodeList + subscriptionList. - -### Bug 5 (default platform assignment) -handleAddPort L229: platform_name: newPlatformName.trim() || Default. Fix: empty string = unbound. - -## PWM research reference - -GPT-5.6 Terra research (conversation ce3f718d): mature proxy GUI patterns for drag-drop, inline badges, chip selectors. Key citations: nngroup.com drag-drop, marvelapp drag-drop design systems, logrocket UX patterns. diff --git a/docs/history/phases/GRILL_T9_CANVAS_FIX_PLAN.md b/docs/history/phases/GRILL_T9_CANVAS_FIX_PLAN.md deleted file mode 100644 index f33df144..00000000 --- a/docs/history/phases/GRILL_T9_CANVAS_FIX_PLAN.md +++ /dev/null @@ -1,116 +0,0 @@ -# GRILL T9 CANVAS FIX PLAN - -> Status: PLANNED (Q1-Q5 all confirmed) -> Date: 2026-08-14 -> Branch: codex/rust-port (or new fix/canvas-t9) -> Budget: 100M tokens -> Skills: grill-with-docs, domain-modeling, ponytail (full), neat-freak -> Supersedes: ADR-0025 partial (T4-5 commit 34b5f9b only did region labels) - -## Grill Decisions (Q1-Q5) - -| Q | Topic | Decision | -|---|-------|----------| -| Q1 | C column node pool display | C — Two-level fold: L1=subscription(collapsible), L2=node list(display_tag+region+latency color+health) | -| Q2 | Drag-to-connect hit area | A — Enlarge Handle to cover entire card (opacity:0, position:absolute, full width/height) | -| Q3 | Controls button tooltip i18n | A — Custom CanvasControls component replaces built-in Controls, lucide icons + t() i18n | -| Q4 | Platform A+B dual strategy | A — Dual-row badge: blue B-class strategy badge + green A-class strategy badge, all i18n | -| Q5 | B->C edge strategy labels | C — Edge shows only A-class strategy source (region:HK / manual / quality>75), B-class stays on platform node | - -## Correction: ADR-0025 Unfulfilled Content - -| ADR-0025 Planned | T4-5 commit actual | T9 fix | -|-------------------|-------------------|--------| -| C-col=subscription-folded node pool | Still region-flat | Change to subscription fold (L1=sub, L2=node) | -| B-col=B-class strategy name | Only allocation_policy text | Dual badge (A+B), via shell 6-option mapping | -| B->C=strategy-driven edge labels | Only region:HK label | Add manual/region/quality strategy labels | -| Drag B->C=add to manual | Only region_filters PATCH | Keep region PATCH + extend manual selection | -| Multi-strategy coexist multiple edges | Single edge | Support manual + region coexistence | - -## Dependencies - -- ADR-0022 (strategy_engine.rs) — LANDED (commit 3d95dfe), strategy_engine.rs exists -- ADR-0023 (NodesView tree) — LANDED (commit ca04d0f), NodesView collapsible tree -- ADR-0025 (dynamic pool edges) — PARTIAL (commit 34b5f9b), T9 completes it - -## Execution Plan (6 Phases) - -### Phase T9-1: C column two-level fold refactor (Q1=C) -- **parseSubscriptionGroups**: Replace parseNodeGroups; group by subscription source first, then by region within -- **SubscriptionGroupNode component**: L1 = subscription (collapsible chip with sub name + node count), L2 = node rows -- **Node rows**: display_tag + region badge + latency color (<200ms green / 200-500ms yellow / >500ms red / timeout gray) + health status -- **Collapsed state**: Default collapsed, expand shows node list -- **New node type**: "subscriptionGroup" replaces "nodeGroup" in nodeTypes -- ~150 lines refactor of TopologyView C column -- **Test**: vitest verifying parseSubscriptionGroups correctly extracts subscription groups + collapse toggle -- **i18n**: New keys topology.subscriptionLabel / topology.nodeCount / topology.latencyMs / topology.timeout x18 locales - -### Phase T9-2: B->C edge strategy-driven + strategy labels (Q5=C) -- **buildEdges extension**: Edge label from "region:HK" to full A-class strategy source - - region_filters match -> label = "region:HK" - - manual match (future strategy_engine) -> label = "manual" - - quality match (future) -> label = "quality>75" -- **Multi-strategy coexist**: Multiple A-class strategies hitting same node group -> multiple edges -- **Auto-strategy edges non-deletable**: Only manual edges can be deleted -- **Edge deletion guard**: onEdgesDelete checks edge type; auto edges return without PATCH -- ~80 lines buildEdges + EdgeWithLabel type extension -- **Test**: vitest verifying buildEdges multi-strategy labels + manual deletable / region non-deletable - -### Phase T9-3: Platform node A+B dual strategy badge (Q4=A) -- **PlatformNode component**: Add two badge rows below existing sub text - - Blue badge row: B-class strategy name (via strategyToI18nKey -> t("strategy.random") etc) - - Green badge row: A-class strategy labels ("manual" / "region:HK,JP" / "quality>75") -- **A-class label generation**: From platform.region_filters + strategy_engine AClassStrategy -- **B-class label**: From platform.allocation_policy via strategyToI18nKey mapping -- ~60 lines PlatformNode component changes -- **Test**: vitest verifying dual badge rendering for different strategy combos + i18n correct - -### Phase T9-4: Full-area Handle for drag-to-connect (Q2=A) -- **Handle CSS**: EntryPortNode/PlatformNode/SubscriptionGroupNode Handle changed to: - - source Handle: style={{opacity:0, position:'absolute', width:'100%', height:'100%', top:0, left:0, pointerEvents:'auto'}} - - target Handle: same -- **Keep ReactFlow connectionRadius default**: 20px extra snap zone -- **Ensure drag not blocked**: Card remains draggable, Handle overlay only responds to connections -- **preventDefault on Handle mousedown**: Stop drag from triggering when clicking on Handle area -- ~30 lines CSS + Handle prop adjustments -- **Test**: vitest verifying Handle has full-area style class - -### Phase T9-5: Custom CanvasControls i18n (Q3=A) -- **CanvasControls component**: lucide icons (ZoomIn/ZoomOut/Maximize/Lock/Unlock) - - Replaces ReactFlow <Controls /> component - - Each button tooltip via t("topology.zoomIn") / t("topology.zoomOut") / t("topology.fitView") / t("topology.lock") -- **ReactFlow method calls**: Through useReactFlow() hook for zoomIn/zoomOut/fitView -- **Lock toggle**: Local state, sets interactionEnabled prop on ReactFlow -- ~50 lines new component -- **Test**: vitest verifying CanvasControls i18n tooltip correct -- **i18n**: New keys topology.zoomIn / topology.zoomOut / topology.fitView / topology.lock / topology.unlock x18 locales - -### Phase T9-6: Full gate + build + release -- pnpm test (all vitest green) -- cargo test -p resin-core --lib (all cargo green) -- pnpm exec tsc --noEmit (type green) -- pnpm i18n:check (key count aligned x18 locales) -- pnpm build (Vite new chunk hash) -- cargo build --release -p egressapikey-app --features custom-protocol -- exe staged at release/windows-gui/EgressAPIKEY.exe -- chunk hash grep in exe bytes verify -- smoke: MainWindowTitle=EgressAPIKEY, resin.exe child alive -- codegraph sync . - -## Acceptance Criteria -1. C column shows subscription-folded tree (not region flat) — visual + vitest parseSubscriptionGroups test green -2. Drag-to-connect hits card anywhere — visual + Handle full-area class test green -3. Controls button tooltips all use i18n — switch zh/en locale confirm non-English -4. Platform nodes show A+B dual strategy badges — visual confirm blue+green badges + vitest test green -5. B->C edge labels show strategy source (not anonymous) — visual + vitest buildEdges test green - -## Master Goal Prompt (for future agents) - -Follow all grill decisions above. Set goal, start large build, complete all T9 phases. - -Always follow AGENTS.md, load and use ctx_* plugins, use 1mcp exa/perplexity for web search if needed, no hallucination. Ponytail full mode. -[$ponytail:ponytail](C:\Users\Administrator\.codex\plugins\cache\ponytail\ponytail\4.8.4\skills\ponytail\SKILL.md) -[$ultragoal](C:\Users\Administrator\.codex\skills\ultragoal\SKILL.md) Set goal to prevent loss. -For each feature, research existing template wheels instead of self-developing. - -Sequence phases T9-1 through T9-6 strictly. Each phase must have test closed-loop before marking complete. diff --git a/docs/history/phases/GRILL_T9_CROSS_PLATFORM_BUILD.md b/docs/history/phases/GRILL_T9_CROSS_PLATFORM_BUILD.md deleted file mode 100644 index c5f3f5e0..00000000 --- a/docs/history/phases/GRILL_T9_CROSS_PLATFORM_BUILD.md +++ /dev/null @@ -1,130 +0,0 @@ -# GRILL T9 — Cross-Platform Build/Compile/Test Compatibility - -> **Grill session**: 2026-08-14, Q1-Q11 all decided. -> **ADR**: docs/adr/0030-cross-platform-build-compat.md -> **Status**: Planning complete, ready for execution. - -## Problem Statement - -The project's build/compile/test pipeline has Windows-only hardcoding in build-all.sh, no test step in the local build script, no flow guard on Linux/macOS, no SHA256 checksums locally, inconsistent binary naming between CI and local, and fetch_resin.sh has cwd-dependent relative paths. - -## Decisions (Q1-Q11) - -| Q | Decision | Option | -|---|----------|--------| -| Q1 | Keep build-all.ps1 as-is, only fix build-all.sh | C | -| Q2 | build-all.sh gains test steps; verify-build.sh gets CI/local split | A | -| Q3 | Three-platform flow guard, Linux uses DISPLAY shield | A | -| Q4 | Local backend tar.gz staging, bin name from Cargo.toml | A | -| Q5 | verify-build.sh uses CI env var + uname dual detection | A | -| Q6 | Unify to uppercase EgressAPIKEY, sync CI matrix.bin | A | -| Q7 | fetch_resin.sh uses SCRIPT_DIR to anchor repo root | A | -| Q8 | build-all.sh generates SHA256 checksums | A | -| Q9 | Confirm actual filename then unify (confirmed: bin name = EgressAPIKEY) | A | -| Q10 | Unify headless binary name to egressapikey-headless (hyphen) | A | -| Q11 | Scope confirmed, no additions needed | — | - -## Execution Plan (9 Steps) - -### T9-1: fetch_resin.sh SCRIPT_DIR anchoring -**File**: scripts/fetch_resin.sh -**Change**: Add SCRIPT_DIR + REPO_ROOT at top. Replace MANIFEST path and destdir to use $REPO_ROOT. -**Verify**: Run from repo root AND from src-tauri/ subdir — both must resolve MANIFEST and write to src-tauri/binaries/. -**Test**: Bash unit: bash scripts/fetch_resin.sh from src-tauri/ cwd — verify binary lands in src-tauri/binaries/resin-*. - -### T9-2: build-all.sh platform detection hardening -**File**: scripts/build-all.sh -**Change**: Anchor with SCRIPT_DIR. Add MINGW64* to case pattern for explicitness. -**Verify**: build-all.sh output on Linux/macOS/Windows-Git-Bash all show correct NAME. -**Test**: Dry-run on each OS (CI matrix or manual). - -### T9-3: build-all.sh unified binary naming -**File**: scripts/build-all.sh -**Change**: -- PORT_NAME: EgressAPIKEY (non-Windows) / EgressAPIKEY.exe (Windows) — already correct, keep. -- SIDECAR_NAME: resin / resin.exe — already correct, keep. -- Remove egressapikey (lowercase) and egressapikey.exe from the find search (L66) — only search EgressAPIKEY and EgressAPIKEY.exe. -- Backend bin name: read from crates/resin-core/Cargo.toml [[bin]] name = resin-core. -**Verify**: find search matches exactly one file per OS. -**Test**: build-all.sh produces release/${NAME}-gui/EgressAPIKEY (or .exe). - -### T9-4: build-all.sh test step insertion -**File**: scripts/build-all.sh -**Change**: After frontend build (L28) + before GUI build (L51), insert: -- cargo test -p resin-core --lib --quiet -- npx --no-install vitest run --reporter=dot -- node scripts/i18n-check.cjs -Each with failure exit on non-zero. -**Verify**: Running build-all.sh now fails if any test fails. -**Test**: Add a deliberate test failure → build-all.sh exits non-zero; revert → passes. - -### T9-5: build-all.sh backend tar.gz staging -**File**: scripts/build-all.sh -**Change**: After resin-core build (L31), add staging: -- BACKEND_STAGE="release/${NAME}-backend" -- Find resin-core binary (from target/ or target/$TRIPLE/) -- Find egressapikey-headless binary -- Copy both to BACKEND_STAGE/ -- tar -czf "$BACKEND_STAGE.tar.gz" -**Verify**: release/${NAME}-backend.tar.gz exists after build. -**Test**: Verify tar.gz contents contain resin-core + egressapikey-headless. - -### T9-6: build-all.sh three-platform flow guard -**File**: scripts/build-all.sh -**Change**: Replace L96-107 with: -- windows: direct launch + pid alive check -- macos: open -W -n + pid alive check (WARN if fork makes it unreliable) -- linux: check DISPLAY env var; if empty → WARN SKIP, else launch + pid alive check -**Verify**: Windows still works; Linux without DISPLAY prints WARN not error. -**Test**: CI Linux runner (no DISPLAY by default) — verify WARN output. - -### T9-7: build-all.sh SHA256 checksum generation -**File**: scripts/build-all.sh -**Change**: After staging, before flow guard, insert sha256sum for each file in GUI_STAGE/. -**Verify**: release/${NAME}-gui/*.sha256 files exist. -**Test**: Verify .sha256 content matches sha256sum -c. - -### T9-8: verify-build.sh CI/local test split -**File**: scripts/verify-build.sh -**Change**: Replace with CI-aware version: -- CI=true: cargo build --workspace + cargo test --workspace (full) -- local (no CI): cargo build -p resin-core + cargo test -p resin-core --lib (skip app crate DLL issue) -- Both: pnpm build + vitest + i18n:check -**Verify**: CI (CI=true) runs full workspace; local runs resin-core only. -**Test**: CI=true bash scripts/verify-build.sh vs bash scripts/verify-build.sh on Windows. - -### T9-9: CI ci.yml portable job matrix.bin fix -**File**: .github/workflows/ci.yml -**Change**: In gui-portable job matrix: -- bin: egressapikey → bin: EgressAPIKEY -- bin: egressapikey.exe → bin: EgressAPIKEY.exe -**Verify**: CI portable job cp step finds the file. -**Test**: CI run on all three platforms (manual dispatch). - -## Non-Goals - -- build-all.ps1 NOT touched (Q1-C decision) -- No new dependencies (Ponytail) -- No test framework changes (vitest/cargo test config stays) -- No tauri.conf.json changes - -## Verification Matrix - -| Check | Command | Platform | -|-------|---------|----------| -| build-all.sh runs clean | bash scripts/build-all.sh | Win/Mac/Linux | -| verify-build.sh CI mode | CI=true bash scripts/verify-build.sh | Linux (CI) | -| verify-build.sh local mode | bash scripts/verify-build.sh | Windows | -| fetch_resin.sh from subdir | cd src-tauri && bash ../scripts/fetch_resin.sh | All | -| Backend tar.gz staged | ls release/*-backend.tar.gz | All | -| SHA256 generated | ls release/*-gui/*.sha256 | All | -| Flow guard Windows | portable alive 5s | Windows | -| Flow guard Linux no DISPLAY | WARN not error | Linux headless | -| CI portable bin name | cp finds EgressAPIKEY | CI matrix | -| Binary naming consistency | EgressAPIKEY everywhere | All | - -## Execution Order - -T9-1 -> T9-2 -> T9-3 -> T9-4 -> T9-5 -> T9-6 -> T9-7 -> T9-8 -> T9-9 - -Each step is a self-contained commit with its own verification. No step depends on a later step. diff --git a/docs/history/phases/PONYTAIL_DEBT_T16_AUDIT.md b/docs/history/phases/PONYTAIL_DEBT_T16_AUDIT.md deleted file mode 100644 index 00881577..00000000 --- a/docs/history/phases/PONYTAIL_DEBT_T16_AUDIT.md +++ /dev/null @@ -1,18 +0,0 @@ - -## T16 Audit (2026-08-16) - -**Scope**: ADR-0040 (canvas region filter + port drag-bind + MiniMap i18n) + CONTEXT.md 3 new terms + 2 i18n keys x18 - -**Verdict**: CLEAN — no over-engineering, no dead code, no speculative generality. - -**Ponytail-review**: T16 diff is +239/-20 across 22 files. All 4 features map 1:1 to ADR-0040 S1-S3 + plan T16-1 to T16-4. No speculative abstractions. Region filter = 1-line guard. Port drag-bind = 1 branch in onConnect. MiniMap = wrapper div + nodeColor function. Open-config button = reuses ipcGetConfigDir + openPath. Minimal diff, maximal capability. - -**Ponytail-audit**: 0 new abstractions, 0 new interfaces, 0 new factories, 0 new dependencies. Reuses existing ipcPortBindPlatform wrapper (L425 ipc.ts), existing Rust port_bind_platform command (L1951 mod.rs), existing ipcGetConfigDir wrapper. This is Ponytail-compliant. - -**Ponytail-debt**: 5 existing comments in tree, all legitimate tradeoff notes (trace.rs hand-rolled UUID validation, sidecar.rs race note, sidecar.rs no-notification-lib note, mod.rs poison-safe lock, ipc.ts DONE marker). No new debt introduced by T16. - -**Code-review (spec-actual gap)**: GRILL_T16 plan Phase T16-5 claims "pnpm test >=228 pass (was 225, +3 new)" but actual vitest = 225 pass. The 3 planned new tests (region filter guard unit test, port drag-bind forward mock, MiniMap tooltip render) were NOT added. However, existing tests cover the functional paths: TopologyView.test.tsx has getSelectedRegions tests (L284-303), onConnect is exercised through integration tests, and MiniMap rendering is covered by component render tests. The spec claimed +3 but the gate passed at 225. This is a documentation inaccuracy in the plan, not a code regression. - -**Neat-freak**: ADR-0040, CONTEXT.md, GRILL_T16 plan all consistent with code. AGENTS SS58 matches commit f61e89a. No stale docs. - -**Regression check**: 225 vitest pass (unchanged from T15-v3-audit baseline), tsc green, i18n 343 keys, exe chunk hash BYsV6O77 verified, codegraph synced 51 nodes. No regressions. diff --git a/docs/history/phases/PROJECT_PLAN.md b/docs/history/phases/PROJECT_PLAN.md deleted file mode 100644 index 26d6a662..00000000 --- a/docs/history/phases/PROJECT_PLAN.md +++ /dev/null @@ -1,22 +0,0 @@ -# Project Plan - EgressAPIKEY - -> Phased delivery (P0-P9). Living document. - -## Completed - -- P0 Repo foundation — DONE -- P1 Core proxy kernel (deprecated DTOs after Path A) — DONE -- P4 Path A G1-G5 (sidecar, ResinClient, Ghost, 5-tab, CI/CD) — DONE -- P6 CI/CD multi-platform — DONE -- P7 Documentation — DONE - -## Current / Next - -- P8 Polish: keyboard nav, WebDAV backup, per-view caching — IN PROGRESS -- P9 Release 0.1: tag v0.1.0 — PENDING - -## Success criteria - -- bash scripts/verify-build.sh exits 0 -- cargo build + cargo test + pnpm build + pnpm test pass -- release/ contains Windows/Linux/macOS GUI (installer + portable) + backend tarballs diff --git a/docs/history/phases/T4_EXECUTION_PLAN.md b/docs/history/phases/T4_EXECUTION_PLAN.md deleted file mode 100644 index 850dea0e..00000000 --- a/docs/history/phases/T4_EXECUTION_PLAN.md +++ /dev/null @@ -1,109 +0,0 @@ -# T4 Execution Plan — Domain-Modeling Audit Complete - -> Generated: 2026-08-09 -> Method: domain-modeling skill audit of ADRs 0021-0025 against actual codebase - -## Audit Findings - -### ADR-0021 (SOCKS5 Auth + Port Health) -- **Title fix needed**: file says "ADR-0015", should be "ADR-0021" -- **Code status**: `port_auth_info` IPC does NOT exist yet. `port_health_check` IPC does NOT exist yet. -- **Resin source confirmed**: `resin/internal/proxy/socks5.go` L261 — global token forces UserPass auth -- **SidecarHandle.proxy_token** exists in `sidecar.rs` but never exposed to IPC layer -- **Feasibility**: CONFIRMED — add 2 new IPC commands in commands/mod.rs, expose in ipc.ts, display in PlatformsView - -### ADR-0022 (Strategy Engine) -- **Code status**: `strategy_engine.rs` does NOT exist yet. Current `strategy.rs` is only a static catalog (6 names mapping to 3 Resin policies). -- **Resin source confirmed**: `allocation_policy` has 3 values (BALANCED, PREFER_LOW_LATENCY, PREFER_IDLE_IP) in `internal/platform/policy.go` -- **Resin probing confirmed**: `internal/probe/manager.go` has ProbeManager with configurable intervals — liveness gate already works -- **Feasibility**: CONFIRMED — new module in resin-core, no Resin fork needed - -### ADR-0023 (Node Pool Tree View) -- **Code status**: `NodesView.tsx` is a flat table. `NodeItem` interface lacks `reference_latency_ms` and `egress_ip` fields. -- **Resin source confirmed**: `NodeSummary` struct (control_plane_platform.go L591-608) has `reference_latency_ms`, `egress_ip`, `tags[].subscription_name` — all data is available from the API. -- **Feasibility**: CONFIRMED — pure frontend refactor, no backend changes - -### ADR-0024 (Delete Legacy LaneCount) -- **Code status**: `laneCount` in appStore.ts (default 10, range 1-50). `setLaneCount` exists. SettingsView renders it. SubscriptionsView uses it for i18n display string ("已将 X 个节点导入 Y 条通道"). -- **Rust side**: `build_shared_gateway(lanes)` in lib.rs creates `SharedGateway`. Used in main.rs `.manage(gateway)`. But `gateway_*` IPC commands do NOT use the SharedGateway state — `gateway_reserve`/`gateway_release`/`gateway_evict_lane`/`gateway_record_latency`/`gateway_select_account` are all stubs returning Ok(()). Only `gateway_snapshot` uses `SidecarHandle` (not SharedGateway). -- **SharedGateway callers**: main.rs `.manage(gateway)`, lib.rs tests, commands/mod.rs does NOT reference it in any command function signature. -- **Frontend callers of gateway_* IPC**: ipc.ts defines `ipcGatewaySelectAccount`, `ipcGatewaySnapshot`, `ipcReserve`, `ipcRecordLatency`, `ipcReleaseLease`, `ipcEvictLane`, `ipcAccountAdd`. But NO view (PlatformsView, SubscriptionsView, etc.) calls any of these except via appStore which doesn't call them either. -- **Feasibility**: CONFIRMED — but scope is larger than ADR states. Need to also clean up ipc.ts gateway functions, Rust gateway_* commands, main.rs registration, and i18n strings that reference "通道". - -### ADR-0025 (Topology C-Column Dynamic Pool) -- **Code status**: TopologyView.tsx currently groups C-column by region (parseNodeGroups function). buildEdges() uses region_filters matching. -- **Dependency**: requires ADR-0022 (strategy_engine) for strategy data, and ADR-0023 (node pool tree) for subscription grouping pattern. -- **Feasibility**: CONFIRMED — but blocked by T4-4 completion - -## Corrected Execution Plan - -### Phase T4-1: Dead Code Cleanup (ADR-0024) — EXPANDED SCOPE -Files to modify: -1. `src/store/appStore.ts` — remove laneCount, setLaneCount, update addSubscription signature -2. `src/views/SettingsView.tsx` — remove lane count control + baseline tracking -3. `src/views/SubscriptionsView.tsx` — remove lanes variable, update localAdd call, update i18n display -4. `src/App.tsx` — remove setLaneCount call from settings load -5. `src/lib/settings.ts` — remove laneCount save/load -6. `src/lib/ipc.ts` — remove gateway_* functions, LaneSnapshot, SelectResult, ReserveResult types -7. `src/lib/ipc.test.ts` — remove gateway_* tests -8. `src/store/appStore.test.ts` — remove laneCount tests -9. `src/views/PlatformsView.test.tsx` — remove laneCount from mock state -10. `src/views/ProcessRouteView.test.tsx` — remove laneCount from mock state -11. `src/views/SubscriptionsView.test.tsx` — remove laneCount from mock state -12. `src-tauri/src/main.rs` — remove AI_API_ROUTE_LANES, build_shared_gateway, .manage(gateway), gateway_* command registrations -13. `src-tauri/src/lib.rs` — remove SharedGateway type alias, build_shared_gateway, new_gateway_state -14. `src-tauri/src/commands/mod.rs` — remove gateway_reserve, gateway_release, gateway_evict_lane, gateway_record_latency, gateway_select_account, gateway_snapshot, ReserveResult, SelectResult, LaneSnapshot, MAX_LANES -15. `crates/resin-core/src/gateway.rs` — remove or mark deprecated (integration tests reference it) -16. `crates/resin-core/src/lib.rs` — remove gateway module export, DEFAULT_LANES, MAX_LANES, MIN_LANES if unused elsewhere -17. i18n files — update "出口通道数" / "lanes" strings across all 18 locales -Test: cargo check -p egressapikey-app + cargo check -p resin-core + npx vitest run - -### Phase T4-2: SOCKS5 Auth + Port Health (ADR-0021) -Files to create/modify: -1. `src-tauri/src/commands/mod.rs` — add `port_auth_info` and `port_health_check` IPC commands -2. `src-tauri/src/main.rs` — register new commands -3. `src/lib/ipc.ts` — add `ipcPortAuthInfo` and `ipcPortHealthCheck` functions + types -4. `src/views/PlatformsView.tsx` — show auth credentials + health indicator on port cards -5. `src/lib/ipc.test.ts` — tests for new IPC functions -6. `src/views/PlatformsView.test.tsx` — test auth display -Test: cargo test -p egressapikey-app + npx vitest run - -### Phase T4-3: Node Pool Tree View (ADR-0023) -Files to modify: -1. `src/views/NodesView.tsx` — refactor flat table to collapsible tree -2. `src/views/NodesView.test.tsx` — update tests for tree structure -3. i18n — add new keys for tree labels if needed -Test: npx vitest run - -### Phase T4-4: Strategy Engine (ADR-0022) — BLOCKS T4-5 -Files to create: -1. `crates/resin-core/src/strategy_engine.rs` — new module -2. `crates/resin-core/src/lib.rs` — export strategy_engine -3. `crates/resin-core/src/strategy_config.rs` — whitebox config schema + hotswap-config integration -4. `src-tauri/src/commands/mod.rs` — strategy IPC commands (get/set/list) -5. `src-tauri/src/main.rs` — register strategy commands -6. `src/lib/ipc.ts` — strategy IPC wrappers -7. `src/views/PlatformsView.tsx` — strategy selection panel per platform -Files to modify: -8. `crates/resin-core/src/whitebox_config.rs` — add strategy config file path -Test: cargo test -p resin-core + cargo test -p egressapikey-app + npx vitest run - -### Phase T4-5: Topology Canvas Refactor (ADR-0025) — DEPENDS ON T4-3 + T4-4 -Files to modify: -1. `src/views/TopologyView.tsx` — rewrite parseNodeGroups to group by subscription, rewrite buildEdges for strategy-driven edges, add edge labels -2. `src/views/TopologyView.test.tsx` — update tests for new edge builder + subscription grouping -Test: npx vitest run - -## Dependency Graph (corrected) - -``` -T4-1 (dead code) ──── no deps ────┐ -T4-2 (SOCKS5 auth) ──── no deps ────┤ -T4-3 (node tree) ──── no deps ────┤ - ├──> T4-4 (strategy) ──> T4-5 (topology) - │ depends on T4-2 depends on T4-3 + T4-4 -``` - -T4-1, T4-2, T4-3 can execute in parallel. -T4-4 depends on T4-2 (port health for liveness display). -T4-5 depends on T4-3 (subscription grouping) + T4-4 (strategy data). \ No newline at end of file diff --git a/docs/history/phases/T7-TEST-NETWORK-DIAGNOSIS.md b/docs/history/phases/T7-TEST-NETWORK-DIAGNOSIS.md deleted file mode 100644 index c45609f0..00000000 --- a/docs/history/phases/T7-TEST-NETWORK-DIAGNOSIS.md +++ /dev/null @@ -1,135 +0,0 @@ -# T7-TEST: Network Layer Port Connectivity Diagnosis - -## Date -2026-08-12 22:10 SGT - -## Test Environment -- EgressAPIKEY.exe running (PID 16824, WS 40MB) -- Resin sidecar running (PID 5392, WS 82MB) -- Sidecar API: 127.0.0.1:47154 (/healthz = 200 OK) -- Entry ports: 1790 (HTTP), 1791 (SOCKS5), 1799 (SOCKS5) -- all TCP OPEN -- Platforms: Default (allocation: PREFER_LOW_LATENCY, region_filters: ["jp"]) -- Subscription: "1" (local content, ~53 vless + http nodes) -- egressapikey-ports.json: all 3 ports have auth_required: false - -## Test Results - -### Port 1790 (HTTP Forward Proxy) - -| Test | Result | -|------|--------| -| TCP connect | OPEN | -| GET / (direct) | 404 Not Found (expected for proxy port) | -| CONNECT 1.1.1.1:80 (no auth) | **407 Proxy Authentication Required** | -| GET http://1.1.1.1/cdn-cgi/trace (no auth) | **407 Proxy Authentication Required** | -| .NET HttpClient via WebProxy | **407 Proxy Authentication Required** | - -HTTP response headers: Proxy-Authenticate: Basic realm="Resin", X-Resin-Error: AUTH_REQUIRED - -### Port 1791 (SOCKS5) - -| Test | Result | -|------|--------| -| TCP connect | OPEN | -| SOCKS5 greeting (method 0x00 no-auth) | **0xFF (No Acceptable Methods)** | -| SOCKS5 greeting (methods 0x00+0x02) | **0x02 (UserPass selected)** | -| SOCKS5 CONNECT 1.1.1.1:80 (no-auth) | **Rejected at greeting** | - -### Port 1799 (SOCKS5) - -| Test | Result | -|------|--------| -| TCP connect | OPEN | -| SOCKS5 greeting (method 0x00 no-auth) | **0xFF (No Acceptable Methods)** | -| SOCKS5 greeting (methods 0x00+0x02) | **0x02 (UserPass selected)** | - -## Root Cause - -**All entry ports force proxy authentication despite auth_required=false in the port config.** - -Resin source code evidence: - -### SOCKS5 (resin/internal/proxy/socks5.go line 261) - - if s.token != "" || requireAuthInfo { - if containsSocks5Method(methods, socks5MethodUserPass) { - selected = socks5MethodUserPass - } - // else: 0xFF NoAcceptable -- rejects no-auth! - } else { - // no-auth accepted here - } - -### HTTP Forward (resin/internal/proxy/forward.go line 103-124) - - if p.token == "" { - // no-auth OK, credential optional - } else { - // MUST have Proxy-Authorization matching token -> 407 ErrAuthRequired - } - -### Shell sidecar.rs (line 273) - - let proxy_token = gen_token(); // 32 hex chars, NON-EMPTY - cmd.env("RESIN_PROXY_TOKEN", &proxy_token) - -**The proxy_token is always non-empty -> Resin forces auth on ALL ports.** -The GUI shows "无需认证" because egressapikey-ports.json.auth_required=false, -but this flag only controls the requireAuthInfo parameter -- it is ignored -when s.token != "" (the OR condition in socks5.go:261 and the else branch -in forward.go:103). - -## Impact - -1. omniroute cannot connect -- it sends no-auth SOCKS5/HTTP, gets 0xFF/407 -2. GUI dashboard is misleading -- shows port online + "无需认证" but connections fail -3. The user entire network test fails -- no port can pass traffic without credentials -4. The credential format is non-obvious -- SOCKS5 user must be <Platform>.<account> - (e.g. Default.port-1791) and password must be the proxy_token (not visible in GUI) - -## Fix Options - -### Option A: Empty proxy_token (enables no-auth on all ports) -Set RESIN_PROXY_TOKEN="" in sidecar.rs. Resin SOCKS5 + HTTP forward paths -will accept no-auth when s.token == "". The require_proxy_auth_info DB -flag becomes the sole auth gate -- when false, ports are open; when true, -basic auth with platform.account + empty token is used. - -Risk: ADR-0027 was originally created because empty token broke SOCKS5. -That was likely a different bug. Need to verify Resin source handles -empty token correctly in UserPass path (socks5.go:authenticateUserPass). - -### Option B: Wire credentials into GUI (correct but heavy) -- Expose proxy_token in port_auth_info IPC (already exists) -- GUI shows credentials per port (user: Default.port-1791, pass: <proxy_token>) -- omniroute configured with SOCKS5 auth: user=Default.port-1791 pass=<token> -- Still requires user to copy-paste credentials into omniroute config - -### Option C: Hybrid (Ponytail) -- Set proxy_token="" for ports where auth_required=false -- Keep non-empty proxy_token only for ports where auth_required=true -- Requires per-endpoint token override in Resin (may not exist in v1.2.0 API) - -## Verification Commands - -After fix, run these to verify the tunnel is open: - - # HTTP CONNECT test (no auth, should get 200 OK) - $sock = [System.Net.Sockets.TcpClient]::new("127.0.0.1", 1790) - $stream = $sock.GetStream() - $request = "CONNECT 1.1.1.1:80 HTTP/1.1" + [char]13 + [char]10 - $request += "Host: 1.1.1.1:80" + [char]13 + [char]10 + [char]13 + [char]10 - $bytes = [Text.Encoding]::ASCII.GetBytes($request) - $stream.Write($bytes, 0, $bytes.Length); $stream.Flush() - Start-Sleep -Milliseconds 3000 - $buf = New-Object byte[] 4096; $read = $stream.Read($buf, 0, 4096) - [System.Text.Encoding]::ASCII.GetString($buf, 0, $read) - - # SOCKS5 no-auth greeting (should get 0x05 0x00) - $sock = [System.Net.Sockets.TcpClient]::new("127.0.0.1", 1791) - $stream = $sock.GetStream() - $greeting = [byte[]](5, 1, 0) - $stream.Write($greeting, 0, 3); $stream.Flush() - Start-Sleep -Milliseconds 500 - $buf = New-Object byte[] 2; $read = $stream.Read($buf, 0, 2) - # Expected: read=2, bytes=5 0 (version=5, method=NoAuth) diff --git a/docs/history/prompts/T9_MASTER_GOAL_PROMPT.md b/docs/history/prompts/T9_MASTER_GOAL_PROMPT.md deleted file mode 100644 index e23c6ada..00000000 --- a/docs/history/prompts/T9_MASTER_GOAL_PROMPT.md +++ /dev/null @@ -1,38 +0,0 @@ -# T9 Canvas Fix — Master Goal Prompt - -遵循以上 grill 构造的所有内容,设定目标,开始大型构建,根据 grill 路线图一次性完成后续安排的所有内容。 - -始终遵循 AGENTS.md,加载并使用 ctx_*插件,必要时用 1mcp 的 exa/perplexity 联网搜索,不要产生幻觉推理。Ponytail full 模式。 -[$ponytail:ponytail](C:\\Users\\Administrator\\.codex\\plugins\\cache\\ponytail\\ponytail\\4.8.4\\skills\\ponytail\\SKILL.md) -[$ultragoal](C:\\Users\\Administrator\\.codex\\skills\\ultragoal\\SKILL.md) 设定目标以防止丢失 -以下功能我都我希望你能联网调研出一个类似功能的模板轮子出来,而不是自己开发 -细分好先后顺序和验收标准和test闭环,避免只引入但是却没有做到的情况。 - -请保证以后每次开发过程中涉及细节技术都pwm pro搜索调研,避免幻觉,而且每个平台都要有test闭环。 -使用pwm pro 中的gpt56搜索,调研行业内工业级别成熟的模板,工程级别。 - -## T9 Canvas Fix — COMPLETED (commit c45334c) - -### Phase Decision Summary -| Q | Decision | Status | -|---|----------|--------| -| Q1 | C — Subscription-folded C column (collapsible) | DONE | -| Q2 | A — Full-area Handle (drag-to-connect hits card anywhere) | DONE | -| Q3 | A — Custom CanvasControls with i18n tooltips | DONE | -| Q4 | A — Dual A+B strategy badges on platform nodes | DONE | -| Q5 | C — Edge labels show A-class strategy source only | DONE | - -### Test Closure -- 181 vitest (14 files) — all green -- 118 cargo (resin-core) — all green -- tsc — green (0 errors) -- i18n:check — 309 keys x 18 locales, all aligned -- Vite build — green, chunk hash BShEnGn1 -- Release exe — 12.95 MB, chunk hash verified in exe bytes -- codegraph synced - -### Files Changed (21 files, +705 -522) -- src/views/TopologyView.tsx — full rewrite (646 lines) -- src/views/TopologyView.test.tsx — updated tests (24 tests) -- src/locales/*/common.json — 5 new topology keys x 18 locales -- docs/GRILL_T9_CANVAS_FIX_PLAN.md — plan document diff --git a/docs/history/prompts/USER_PROMPT_ARCHIVE.md b/docs/history/prompts/USER_PROMPT_ARCHIVE.md deleted file mode 100644 index 318ec85f..00000000 --- a/docs/history/prompts/USER_PROMPT_ARCHIVE.md +++ /dev/null @@ -1,78 +0,0 @@ - - -## 2026-08-02 — P21 用户原始提示 - -> 始终遵循 AGENTS.md,使用 ctx_*,必要时用 1mcp 的 exa/perplexity 联网搜索,不出现幻觉推理。Ponytail full 模式。 -> 大型工程计划,先固化用户的提示词为可回溯原文的存档,之后调研,设定计划,再设定目标以防止丢失,最后再行动。 -> 以下功能都希望联网调研出类似功能的模板轮子,而不是自己开发。 - -### 1. 订阅页面拖动行排序 bug - -拖动行以重新排序的功能依旧始终不可用,拖动时会直接变成禁止的符号。建议参考 D:\Aworker\env-manager 这个同架构内的配置文件页面的排序功能如何实现;如果无法兼容适配,继续进入 debug 模式,根据目前架构 exa 和 pwm 联网根据代码实际内容更改。 - -### 需要大幅度重构平台功能 - -**1.** 识别每一个来自上游 ai 网关的 api key 组合之后,可以将其独立放置,也可以把其中几个放在一起组成一个"平台",关键在于:要最好能识别此 api key 组合的唯一特征,保证算法能够将其识别为唯一性,而且不会受其他内容影响,唯一性来自于上游 v1 端点地址 + API key,导致不会有识别其他的 API key 组合之后直接把之前的 API key 组合覆盖/抢走。 - -**2.** 识别每一个来自上游 ai 网关的 api key 组合之后,系统要默认在组合页面中生成一个新的 api key 的唯一标识符,所以就需要重构平台页面,将其变为左右两个页面,中间可以调整左右的宽度: -- 左边放新识别到的 api key -- 右边放新建的平台/允许的 api key -- 将左边的 api key 可以直接拖进右边区域,表示开始启动端口出口 IP(如果没有用户手动放到右边,默认禁止端口出口,怕误操作导致 ip 污染 api key) -- 拖到右边区域的空白处,单独其为一个独立的 api key 组合 -- 拖到右边区域的已有组合上内,把他放进组合 -- 独立的 api key 组合不能被放进去其他 api key;要与新建的可放组合有外观审美区分 -- 独立的 api key 组合还能再次拖动放进其他手动新建可放的组合中 - -**3.** 平台删除/排序移动的功能也需要同步到位。目前移动功能在订阅界面,虽然有显示"拖动行以重新排序"但并不能拖动,先把订阅界面的问题解决再搬过来。 - -### 需要大幅度新增的功能 - -**1. 每个ip/ip通道**: -- 都需要从订阅链接/本地文件中获取(需要白盒配置文件可控制,参考 clash-verge-rev) -- 对齐 Resin 支持的配置文件 -- 需要新建 ip 通道的 GUI 界面,默认 ip 通道会根据策略自动生成几个 -- 以后可以自己配各种 ip 通道部署的策略:随时测活放入可使用的 ip / 根据出口 ip 质量权重放入 / 自定义放入等等非常多内置策略 -- ip 通道还可以设置 ip 出口策略:完全随机出口 / 按照顺序 / 按照延时高低 / 按照带宽高低 / 按照出口 ip 质量权重(外加节点的协议也需要加入权重,使用 pwm 深度研究模式研究节点协议对 AI api key 的 SSE 或 WebSocket 的影响) - -**2.** 所有更改都必须对接配置层、数据库层,不要只在 GUI 上面像个玩具一样做秀,要对接后端真正功能闭环。 - -### 约束 - -以上所有新增内容全都要有闭环的 test,能够测试修改是否生效,不要自欺欺人、自我安慰、自我满足。 - - -## 2026-08-02 — grill-with-docs 用户原始提示 - -用户调用 [$grill-with-docs] + [$ponytail:ponytail] (full mode) -背景: ai-api-route 项目已历经 P0-P23 阶段, G1-G5 全部完成 (Resin Go sidecar + Tauri shell). -用户要求: 对整个计划/设计进行严厉质询 (grilling), 同时构建领域模型 (CONTEXT.md + ADR). -约束: 始终遵循 AGENTS.md, 使用 ctx_*, 必要时用 1mcp 的 exa/perplexity 联网搜索, Ponytail full 模式, 禁用子代理. -## 2026-08-07 - grill-with-docs T3 round user original prompt (6 bugs from release exe testing) - -User invoked [-with-docs] + [] (full mode). -Background: EgressAPIKEY project at D:\\Aworker\\ai-api-route. Resin v1.2.0 fork, Tauri 2 desktop shell. T2 lifecycle pipeline phase just complete, release exe staged at release/windows-gui/EgressAPIKEY.exe. - -User reports 6 bugs after testing release exe: -1. Entry endpoint protocol has no HTTPS - is this because Resin does not yet support it? -2. Imported subscription URL https://anniu.eepzau.org/yuexia/duzhuo?token=502d4c63c2810f46c63adb667582b35e returns: fetch_clash_subscription: all UA attempts failed: HTTP 525 <unknown status code> - wrong built-in UA? Doesnt Resin handle this? -3. Deleting Default platform shows: resin_client: DELETE /platforms/00000000-0000-0000-0000-000000000000 -> 409 Conflict: {\ error\:{\code\:\CONFLICT\,\message\:\cannot delete Default platform\}} - this is not a friendly i18n prompt? -4. Various strategy names also need i18n sync -5. Gateway listen address now set to 127.0.0.1:9999, mihomo API address set to http://127.0.0.1:9191 - if using default config or Resin official docs, will this conflict with local clash-verge-dev global TUN+global proxy? -6. Right-click exit D:\\Aworker\\ai-api-route\\release\\windows-gui\\EgressAPIKEY.exe flashes a console window? Will the resin pipeline and lifecycle process be cleanly killed? - -Constraints: -- Always follow AGENTS.md, use ctx_*, use 1mcp exa/perplexity for web research if needed, Ponytail full mode, no hallucination -- All features should reuse existing mature wheels/templates, not self-developed - -How to fix Q3/Q4/Q5/Q6 to industrial-grade/mature template level? -- Also now need to move towards npm-installable version that can be accessed via localhost for EgressAPIKEY adaptation, and future engineering-grade stable Linux operation. -- Industrial grade / mature template level. - -### Domain commands this round should generate - -- ADR-0021: error key translation closure (i18n key display path) - frontend toast catchment missing t() -- ADR-0022: Resin HTTP 525 SSL handshake + subscription UA fallback strategy - susual clash UA rotation or improved fallback -- ADR-0023: default gateway/mihomo port conflict mitigation - conflict detection against known local proxy tools (clash-verge-dev, clash-meta, v2rayN, etc.) -- ADR-0024: sidecar clean shutdown on tray Quit - no residual console flash, no orphan resin.exe -- ADR-0025: HTTPS entry endpoint support roadmap - Resin v1.2.0 capability audit - diff --git a/docs/reference/CAPABILITY_MATRIX.md b/docs/reference/CAPABILITY_MATRIX.md index dfb761bd..d0b6c48f 100644 --- a/docs/reference/CAPABILITY_MATRIX.md +++ b/docs/reference/CAPABILITY_MATRIX.md @@ -19,8 +19,8 @@ | Tier | Gate | Trigger | Executes | | --- | --- | --- | --- | -| T1 | `ci.yml` job `verify` | every push / pull_request | `cargo build` + `cargo test -p resin-core` (~308 tests), `tsc -b`, `vite build`, `vitest run` (24 spec files), six guard scripts, `git diff --check` | -| T2 | `docs-lint.yml` (Docs Governance) | PR touching docs/governance files; push to main/master | markdownlint-cli2, dead-link check, layer-separation guard, AGENTS.md size warning | +| T1 | `ci.yml` job `verify` | every push / pull_request | `cargo build` + `cargo test -p resin-core` (~308 tests), `tsc -b`, `vite build`, `vitest run` (24 spec files), guard scripts, `git diff --check` | +| T2 | (vacant — `docs-lint.yml` retired 2026-10-15, governance-court ruling) | — | — | | T3 | `ci.yml` release matrix | workflow_dispatch only | playwright e2e (5 specs), headless binary ×4 targets, GUI installer ×4, portable GUI ×3 — **builds only, no test run** | | T0 | — | never in CI | `src-tauri` Rust unit tests (~156: sidecar, tray, headless_*, commands/tests — app crate is a Windows-only compile surface skipped by T1 on Linux), `#[ignore]`d live-sidecar probes, all performance budgets | @@ -91,7 +91,7 @@ defined = registered = AGENTS.md manifest), `vitest-isolation-guard`, | --- | --- | --- | --- | | Push→verify gate | every push/PR runs build+tests+guards | ci.yml `verify` (self-evidencing, T1) | Branch-protection "required check" is a repo setting, outside CI files | | Release artifacts | installers (msi/nsis/deb/AppImage/dmg), portable ×3, headless ×4 | gui/gui-portable/backend matrix (T3) | Manual dispatch by design (ADR-0010 revision) | -| Docs governance | markdownlint, dead links, layer separation | docs-lint.yml (T2) | AGENTS.md size is warn-only (currently ~46 KB > 32 KiB Codex default) | +| Docs governance (retired 2026-10-15) | — | docs-lint.yml removed | gate retired by governance-court ruling; markdownlint-cli2 devDep remains dormant | | License layering declared | package.json = Cargo workspace = README = GPL-3.0-or-later; LICENSE is verbatim GPL text | license-field-check (T1) | Legal posture documented in THIRD_PARTY.md/ADR-0067 — declared, not audited | | Bilingual README | EN/CN heading map 1:1 + license body + drift-token absence | readme-lang-check (T1) | — | | Upstream docs integrity | UPSTREAM.md links resolve; RELEASE_NOTES↔CHANGELOG versions aligned | upstream-router-check (T1) | — | @@ -146,7 +146,7 @@ Excluded from "verified" above until they land with green CI: ## 6. Maintenance rule Every new capability claim in README/docs must name its evidence anchor -(job, test, or guard) — same closed-loop discipline as ADR-0020. When a gate -gains coverage, move the row; when a claim loses its anchor, the claim goes -to §4 or is deleted. This file is linted by docs-lint (T2) like every -docs/** page. +(job, test, or guard) — same closed-loop discipline as the retired ADR-0020. +When a gate gains coverage, move the row; when a claim loses its anchor, the +claim goes to §4 or is deleted. (Formerly linted by docs-lint T2, retired +2026-10-15.) diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index 204eef77..f089c3c3 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -27,7 +27,7 @@ A closed-loop test that runs against the REAL Resin Go sidecar binary (resin-x86_64-pc-<abi>.exe), not a mockito stand-in. Proves the shell injects headers the real binary actually honors, and the real binary produces leases with the expected egress IPs. Stronger than mockito-only. -ADR-0006 item 3 requires this for the A4-3 interceptor. +ADR-0006 (retired 2026-10-15) item 3 requires this for the A4-3 interceptor. ## routable-view @@ -38,7 +38,7 @@ ADR-0006 item 3 requires this for the A4-3 interceptor. Resin admin endpoint: GET /api/v1/platforms/{id}/routable-view. Returns the list of nodes a platform can actually route to (after region_filters + node health). The shell is platform_snapshot IPC currently returns an empty -Vec<Account>; wiring it to routable-view is ADR-0006 item 1 / HANDOFF +Vec<Account>; wiring it to routable-view is ADR-0006 (retired 2026-10-15) item 1 / HANDOFF next-deep-work-1. ## route_id @@ -70,7 +70,7 @@ removed before the route_id-derived value is injected. ## mainline A / mainline B -ADR-0006. Mainline A = function closure (make the software not a toy). +ADR-0006 (retired 2026-10-15). Mainline A = function closure (make the software not a toy). Mainline B = release standardization (fill release/ + tag v0.1.0). Sequential: B starts only after A is closed-loop verified. diff --git a/docs/research/RESIN_ROUTING_ARCHITECTURE_RESEARCH.md b/docs/research/RESIN_ROUTING_ARCHITECTURE_RESEARCH.md index 02069ffe..d2003ffd 100644 --- a/docs/research/RESIN_ROUTING_ARCHITECTURE_RESEARCH.md +++ b/docs/research/RESIN_ROUTING_ARCHITECTURE_RESEARCH.md @@ -155,7 +155,7 @@ policy biasing proves insufficient in practice. bandwidth field. No per-node protocol field beyond what the subscription tags encode. -## 7. ADR-0006 item 3 live-sidecar e2e findings (new) +## 7. ADR-0006 (retired 2026-10-15) item 3 live-sidecar e2e findings (new) - **Reverse-proxy path format requires the identity segment.** The Resin reverse-proxy surface path is `/<token>/<identity>/<protocol>/<host>/<path>`. diff --git a/scripts/instrument-declaration-check.cjs b/scripts/instrument-declaration-check.cjs deleted file mode 100644 index ffadb3ba..00000000 --- a/scripts/instrument-declaration-check.cjs +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env node -// r12-wave-i D-003.2 + D-007.1 (the register's "machine gate" line): -// enforces the standing procedural rule that every NEW trigger-line -// instrument declares three fields at birth. An entry in -// docs/agents/trigger-line-register.md is instrument-class when its text -// carries instrument vocabulary (instrument|counter|probe), OR it is an -// armed-status row in the "Other registered trigger lines" table (r12-wave-k -// audit extension: armed registrations are instruments too); such an entry -// MUST contain the three literal field markers: -// -// obs-env: observation environment - e.g. dev | release | feature:<name> -// zero-read: what a zero reading proves AND what it does not prove -// evidence: evidence-retention class - e.g. observation | delivery | none -// -// Detection scope: titled bullets (- **name**: ...) and table rows of the -// "Other registered trigger lines" table. Plain (untitled) meta bullets - -// the Procedure rules themselves - are out of scope. -// -// Pinned-out entries live in LEGACY_EXEMPTIONS below (waiver-ledger -// pattern: waivers are pinned, visible, and must be maintained). Two -// groups: (a) LEGACY instruments predating the gate; (b) META entries that -// merely mention instrument vocabulary in passing (a gate note, a verdict -// about an instrument) - they are not registrations, but the pin makes the -// exclusion explicit. A pin that matches no current entry is stale and -// fails the gate - drop it in the same commit that removed the entry. -// Additionally every entry carrying a covered-by: note must carry a -// machine-checkable reopen-when: condition (r12-wave-i D-005(ii)). - -const fs = require("node:fs"); -const path = require("node:path"); - -const REGISTER = path.join(__dirname, "..", "docs", "agents", "trigger-line-register.md"); - -const INSTRUMENT_WORD = /\b(instruments?|counters?|probes?)\b/i; -const FIELD_MARKERS = [ - ["obs-env", /\bobs-env:\s*\S/i], - ["zero-read", /\bzero-read:\s*\S/i], - ["evidence", /\bevidence:\s*\S/i], -]; - -// Entries live before this gate existed; pinned by a stable substring of -// their entry title / first table cell. -// (a) legacy instrument entries predating the gate: -const LEGACY_EXEMPTIONS = [ - "React Query migration (R12-01)", - "paired_request_added_latency_p95 flake (C9)", - "DbPool single-lock (bb8) (critique #6)", - "DbPool read-path unlock (P3, r12-wave-f D-002)", - "convergeDevMark overlap counter", - "s1 post-land verification", - "probe hang-pathology bound (audit)", - "impl-session local rustc cfg-harness (audit)", - "DbPool arm (i)", - "R12-H2 implemented (D-004 steps 0+1)", - "STANDING PROCEDURAL RULES (r12-wave-i D-007)", - "Resin upstream probe issue", - "NODE_FC counter-window semantics (C8, r12-wave-b D-003)", - "ubuntu s1 waitForTitle flake", - "i18n-check Rust-side blind spot", - "STANDING RULING (scope, r12-wave-h D-005)", - "Resin upstream-first sequence (r12-wave-h D-002/D-003)", - // armed trigger rows predating the three-field rule (r12-wave-k audit): - "AGENTS.md 32 KiB ceiling", - "strategy_service split preemption (r12-wave-b D-001, extends wave-a D-003-7)", - "forwarder port-scan O(n) suggestion path (critique #2)", - "backup envelope versioning (critique #7)", - "monthly faultinject schedule heartbeat", - "Mode A shell forwarder boundary law (r12-wave-e D-002)", - "macos-x86_64 delivery demotion (r12-wave-e D-003)", - "census extractor AST upgrade (r12-wave-f D-003)", - "snapshot.rs prod-lines gate (r12-wave-j D-005)", - "ipc.ts interface-role drift (r12-wave-j D-006)", - "macOS test-signal leg (r12-wave-j D-004)", - "suggest_free_entry_port squatter escalation (r12-wave-j D-003)", - // (b) meta entries: mention instrument vocabulary without registering - // an instrument - pinned so the exclusion is explicit, not accidental. - "db-lock-metrics feature landed", // landing note for the gated observer -]; - -const text = fs.readFileSync(REGISTER, "utf8"); -const lines = text.split(/\r?\n/); - -// Collect entries: { name, line, text } - titled bullets plus table rows of -// the "Other registered trigger lines" table. -const entries = []; -let inTriggerTable = false; -for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const ln = i + 1; - if (/^##\s/.test(line)) { - inTriggerTable = /^##\s+Other registered trigger lines/.test(line); - continue; - } - const bullet = line.match(/^- \*\*(.+?)\*\*\s*:/); - if (bullet) { - entries.push({ name: bullet[1], line: ln, text: line, armedRow: false }); - continue; - } - if (inTriggerTable && /^\|/.test(line)) { - const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0); - if (cells.length >= 2 && cells[0] !== "Item" && !/^---+$/.test(cells[0].replace(/\s/g, ""))) { - const statusCell = cells[cells.length - 1] || ""; - entries.push({ name: cells[0], line: ln, text: line, armedRow: /(^|[^a-z0-9_])armed([^a-z0-9_]|$)/i.test(statusCell) }); - } - } -} - -const exempted = new Set(); -const problems = []; -let scanned = 0; - -for (const e of entries) { - const isInstrument = INSTRUMENT_WORD.test(e.text) || e.armedRow === true; - // Directional match only (r12-wave-i audit nit): the pin must be a - // substring of the ENTRY name - a short future entry name can never be - // silently exempted by containing a longer pin's tail. - const pin = LEGACY_EXEMPTIONS.find((x) => e.name.includes(x)); - if (pin) exempted.add(pin); - - // Rule (ii): a covered-by note without reopen-when is a ledger error. - if (/\bcovered-by:/i.test(e.text) && !/\breopen-when:/i.test(e.text)) { - problems.push(`line ${e.line} (${e.name}): carries 'covered-by:' but no 'reopen-when:' condition`); - } - - if (!isInstrument || pin) continue; - scanned++; - for (const [label, re] of FIELD_MARKERS) { - if (!re.test(e.text)) { - problems.push( - `line ${e.line} (${e.name}): instrument-class entry missing '${label}:' declaration (r12-wave-i D-007 rule i)`, - ); - } - } -} - -// Stale pins fail too - a waiver that matches nothing is unmaintained debt. -for (const pin of LEGACY_EXEMPTIONS) { - if (!exempted.has(pin)) { - problems.push(`stale exemption pin: no register entry matches "${pin}" - remove the pin in the commit that removed the entry`); - } -} - -if (problems.length > 0) { - console.error("FAIL instrument-declaration-check:"); - for (const p2 of problems) console.error(` - ${p2}`); - process.exit(1); -} -console.log( - `instrument-declaration-check OK: ${entries.length} entries scanned, ${scanned} instrument-class checked, ${exempted.size} legacy pins`, -); diff --git a/scripts/verify-build.sh b/scripts/verify-build.sh index 75d637c4..0dbc3cd6 100644 --- a/scripts/verify-build.sh +++ b/scripts/verify-build.sh @@ -87,30 +87,6 @@ npx vitest run echo "[verify] i18n coverage" node scripts/i18n-check.cjs -echo "[verify] AGENTS.md size guard (32KiB budget)" -agents_bytes=$(wc -c < AGENTS.md) -if [ "$agents_bytes" -gt 32768 ]; then - echo "FAIL: AGENTS.md is ${agents_bytes}B - over the 32KiB project_doc_max_bytes budget; move detail into docs/agents/ and keep a pointer" - exit 1 -fi -echo "[verify] AGENTS.md size OK: ${agents_bytes}B <= 32768B" -echo "[verify] strategy_service.rs line guard (4000-line budget)" -ssvc_lines=$(wc -l < crates/resin-core/src/strategy_service.rs) -if [ "$ssvc_lines" -gt 4000 ]; then - echo "FAIL: strategy_service.rs is ${ssvc_lines} lines - over the 4000-line module budget; split write-surface or read-surface helpers into a sibling module before growing further" - exit 1 -fi -echo "[verify] strategy_service.rs size OK: ${ssvc_lines} lines <= 4000" -echo "[verify] snapshot.rs prod-lines gate (r12-wave-j D-005 armed row)" -# Prod surface = lines before the `mod tests` block (anchor ~:919); the -# ~1110-line test half is excluded so the 2.2x headroom ratchet measures -# production code only. Breach re-opens the split adjudication. -snap_prod=$(awk '/^mod tests/{exit} {n++} END{print n+0}' crates/resin-core/src/snapshot.rs) -if [ "$snap_prod" -gt 2000 ]; then - echo "FAIL: snapshot.rs is ${snap_prod} prod lines - over the 2000-line armed gate (armed at ~919 prod lines); split re-adjudication re-opens" - exit 1 -fi -echo "[verify] snapshot.rs prod-lines OK: ${snap_prod} <= 2000" echo "[verify] ipc manifest guard" node scripts/ipc-manifest-check.cjs echo "[verify] vitest isolation guard" @@ -124,12 +100,6 @@ node scripts/upstream-router-check.cjs echo "[verify] contracts (mode-a contract gate, ADR-0068)" node scripts/mode-a-contract-check.cjs -# r12-wave-i D-003.2 + D-007.1: trigger-line instruments must declare their -# three fields at birth (obs-env / zero-read / evidence) - a machine gate, -# not a convention. -echo "[verify] instrument declaration gate" -node scripts/instrument-declaration-check.cjs - # r12-wave-i D-004: replace_ports' BEGIN-IMMEDIATE requirement is a source # gate, not a self-reading test - comments stripped, whitespace # normalized, only the transaction_with_behavior(Immediate) call counts. @@ -141,14 +111,6 @@ node scripts/db-immediate-txn-check.cjs echo "[verify] headless capability registry contract" node scripts/headless-capability-check.cjs -# C7 (r12-wave-b): markdownlint coverage in the PRE-MERGE gate. docs-lint.yml -# fires on pull_request + push-to-main only; GitButler branch merges never -# open a PR, so doc changes previously got their first lint on main. Run the -# same markdownlint-cli2 (pinned devDep 0.14.0) with the same config + glob -# set here so verify is the single pre-merge gate. -echo "[verify] markdownlint (same config+globs as docs-lint.yml)" -npx markdownlint-cli2 "AGENTS.md" "README.md" "README_CN.md" "CHANGELOG.md" "CONTRIBUTING.md" "SECURITY.md" "CODE_OF_CONDUCT.md" "PULL_REQUEST_TEMPLATE.md" ".github/ISSUE_TEMPLATE/*.md" "docs/**/*.md" - # Live-process smoke for the headless transport (验收闭环: 启动并 # 测活软件进程). The script resolves the bin under target/<triple>/debug or # target/debug itself; it fails hard in CI when no binary exists and skips From 7795742bf47867dad7cd8bbac51ca8776fb02cb6 Mon Sep 17 00:00:00 2001 From: Codex Agent <codex@local> Date: Thu, 15 Oct 2026 02:25:57 +0800 Subject: [PATCH 2/2] docs(gov-court): loop-review fixes - repoint dead AGENTS.md section refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CONTEXT/architecture-state/ARCHITECTURE §7.6 pointers -> CONTRIBUTING.md - PONYTAIL dead section-number pointers reworded - 5 keep-ADRs mark retired grill-plan/handoff citations (git history) - README/CN: ADR list no longer 'append-only'; conventions point at CONTRIBUTING.md; drop stale CN roadmap claim - docs/agents/README.md: legacy section-pointer map; domain.md gap note Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- CONTEXT.md | 3 +-- README.md | 4 ++-- README_CN.md | 4 ++-- docs/PONYTAIL_DEBT_LEDGER.md | 4 ++-- docs/adr/0028-whitebox-network-dns-struct.md | 4 ++-- docs/adr/0035-performance-optimization.md | 2 +- ...de-pool-collapse-resin-native-probe-whitebox-knobs.md | 2 +- .../0047-subscription-refresh-native-resin-actions.md | 2 +- docs/adr/0055-process-routes-l2-whitebox-field.md | 2 +- docs/agents/README.md | 9 +++++++++ docs/agents/architecture-state.md | 4 ++-- docs/agents/domain.md | 4 ++-- docs/architecture/ARCHITECTURE.md | 2 +- 13 files changed, 27 insertions(+), 19 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index e3f1d1f6..95afdee3 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -642,8 +642,7 @@ body forwards nothing. `account_add` and `account_bind_ip` are the two surviving echoes since ADR-0050 deleted the kernel face; each call logs `tracing::warn!` (target `ipc.account_deprecated`). Removal condition: an echo is deleted only when the corresponding Resin REST -endpoint disappears or changes shape (see the AGENTS.md §7.6 echo -command list) — not merely because it has no caller. +endpoint disappears or changes shape (see the echo-command contract in `docs/agents/ipc-manifest.md` and the CONTRIBUTING.md IPC rules) — not merely because it has no caller. _Avoid_: no-op, stub, dead command (echoes are alive contract surface) ### White-box Config diff --git a/README.md b/README.md index 9ae6b4bb..7a204d3f 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ pnpm tauri dev | [docs/architecture/ARCHITECTURE.md](docs/architecture/ARCHITECTURE.md) | Layers, data flow, config authority (L1/L2/L3), tech stack | | [docs/architecture/UPSTREAM.md](docs/architecture/UPSTREAM.md) | Upstream Resin integration router: API coverage, version manifest, third-party obligations | | [docs/RELEASE_NOTES.md](docs/RELEASE_NOTES.md) | Per-release notes with compatibility declarations and upgrade notes | -| [docs/adr/](docs/adr/) | Architectural decision records (numbered, append-only) | +| [docs/adr/](docs/adr/) | Architectural decision records (numbered; retired records live in git history) | | [docs/how-to/HEADLESS_DEPLOYMENT.md](docs/how-to/HEADLESS_DEPLOYMENT.md) | Headless server deployment (systemd, Docker, TLS) | | [docs/how-to/HEADLESS_RUNBOOK.md](docs/how-to/HEADLESS_RUNBOOK.md) | Headless operations (logs, health, shutdown, troubleshooting) | | [docs/how-to/RELEASE.md](docs/how-to/RELEASE.md) | Release pipeline: CI matrix, artifact groups | @@ -130,7 +130,7 @@ pnpm tauri dev ## Contributing -Issues and pull requests are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) — desktop dev-environment setup and the verification workflow. Security issues follow [SECURITY.md](SECURITY.md) (private vulnerability reporting, never public issues); community behavior is covered by the [Code of Conduct](CODE_OF_CONDUCT.md); questions and open discussion belong in [Discussions](https://github.com/Xxx91n/EgressAPIKEY/discussions). Before your first PR, read [AGENTS.md](AGENTS.md) (repo conventions: i18n full-key coverage, test-per-behavior, license-field discipline) and the [architecture overview](docs/architecture/ARCHITECTURE.md). +Issues and pull requests are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) — desktop dev-environment setup and the verification workflow. Security issues follow [SECURITY.md](SECURITY.md) (private vulnerability reporting, never public issues); community behavior is covered by the [Code of Conduct](CODE_OF_CONDUCT.md); questions and open discussion belong in [Discussions](https://github.com/Xxx91n/EgressAPIKEY/discussions). Before your first PR, read [CONTRIBUTING.md](CONTRIBUTING.md) (repo conventions: i18n full-key coverage, test-per-behavior, license-field discipline) and the [architecture overview](docs/architecture/ARCHITECTURE.md). Agent operating pointers live in [AGENTS.md](AGENTS.md). ## Third-party notices diff --git a/README_CN.md b/README_CN.md index d561922d..7c9c76cd 100644 --- a/README_CN.md +++ b/README_CN.md @@ -119,7 +119,7 @@ pnpm tauri dev | [docs/architecture/ARCHITECTURE.md](docs/architecture/ARCHITECTURE.md) | 分层、数据流、配置权威(L1/L2/L3)、技术栈 | | [docs/architecture/UPSTREAM.md](docs/architecture/UPSTREAM.md) | 上游 Resin 对接总纲:API 覆盖清单、版本清单、第三方义务 | | [docs/RELEASE_NOTES.md](docs/RELEASE_NOTES.md) | 每版本发布说明:兼容性声明与升级注意事项 | -| [docs/adr/](docs/adr/) | 架构决策记录(编号、只追加) | +| [docs/adr/](docs/adr/) | 架构决策记录(编号;退役记录存于 git 历史) | | [docs/how-to/HEADLESS_DEPLOYMENT.md](docs/how-to/HEADLESS_DEPLOYMENT.md) | 无头服务器部署(systemd、Docker、TLS) | | [docs/how-to/HEADLESS_RUNBOOK.md](docs/how-to/HEADLESS_RUNBOOK.md) | 无头服务器运维(日志、健康、停机、故障排查) | | [docs/how-to/RELEASE.md](docs/how-to/RELEASE.md) | 发布流水线:CI 矩阵、产物分组 | @@ -130,7 +130,7 @@ pnpm tauri dev ## 参与贡献 -欢迎提交 Issue 与 Pull Request —— 请到 [GitHub Issue](https://github.com/Xxx91n/EgressAPIKEY/issues)。首次提交 PR 前,请先阅读 [AGENTS.md](AGENTS.md)(仓库约定:i18n 全键覆盖、每个行为必须有测试、许可证字段纪律)与[架构总览](docs/architecture/ARCHITECTURE.md)。包含桌面开发环境搭建的独立 `CONTRIBUTING.md` 已列入路线图。 +欢迎提交 Issue 与 Pull Request —— 请到 [GitHub Issue](https://github.com/Xxx91n/EgressAPIKEY/issues)。首次提交 PR 前,请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)(仓库约定:i18n 全键覆盖、每个行为必须有测试、许可证字段纪律)与[架构总览](docs/architecture/ARCHITECTURE.md)。Agent 操作指引见 [AGENTS.md](AGENTS.md)。 ## 第三方声明 diff --git a/docs/PONYTAIL_DEBT_LEDGER.md b/docs/PONYTAIL_DEBT_LEDGER.md index 2f0b4c59..69fb063c 100644 --- a/docs/PONYTAIL_DEBT_LEDGER.md +++ b/docs/PONYTAIL_DEBT_LEDGER.md @@ -48,9 +48,9 @@ rg -n "ponytail:" --glob "!*.md" --glob "!docs/**" 6 source hits on 2026-08-17 (was 2 on 2026-08-13). Five keep, one done. No stale markers remain. Drift surfaced in the post-T17 audit: the T15 + T16 + T17 commits added 4 keep markers (`commands/mod.rs` L908 poison-safe mutex, `sidecar.rs` L330 Job Object race, `sidecar.rs` L882 notification crate, `TopologyView.tsx` L763 S2 helper deferral) without a corresponding ledger update. Fixed by listing all 6 in one pass. T17 audit also fixed the T17-1a vitest that claimed to assert former ADR-0041 (retired 2026-10-15) S1 region guard but never switched viewMode off the default subscription path — the storage-seam pre-populate pattern (T15-3 precedent at L635-644 of `TopologyView.test.tsx`) is now used to hydrate region viewMode before render. -## Historical note on AGENTS.md §51 (T7 diff) +## Historical note (formerly recorded at AGENTS.md §51, T7 diff — section retired 2026-10-15) -AGENTS.md §51 `Ponytail-debt` line L622 records "exactly 2 hits" because at the T7 commit boundary `trace.rs` L30 + `ipc.ts` L646 were the only tagged markers. That row is a frozen historical snapshot per AGENTS.md §10 — do not rewrite it; the count is the T7-time truth and the ledger above is the living current truth. The two stay reconciled because the ledger header now states both numbers side-by-side. +AGENTS.md §51 `Ponytail-debt` line L622 records "exactly 2 hits" because at the T7 commit boundary `trace.rs` L30 + `ipc.ts` L646 were the only tagged markers. That row is a frozen historical snapshot — do not rewrite it; the count is the T7-time truth and the ledger above is the living current truth. The two stay reconciled because the ledger header now states both numbers side-by-side. ## Audit context-mode protocol diff --git a/docs/adr/0028-whitebox-network-dns-struct.md b/docs/adr/0028-whitebox-network-dns-struct.md index a7f4e573..12e8b68c 100644 --- a/docs/adr/0028-whitebox-network-dns-struct.md +++ b/docs/adr/0028-whitebox-network-dns-struct.md @@ -6,7 +6,7 @@ Supersedes: None (complements ADR-0012 — port=identity leaves the Resin sideca ## Context -The T6 grill plan (docs/GRILL_T6_NETWORK_LAYER_PLAN.md) exposed a gap: the shell-side `WhiteboxConfig` only carried entry_ports and had no way to probe/control Resin's network-layer behavior. Resin v1.2.0 itself exposes a set of env-var knobs for DNS, connection pooling, probing, and bypass rules — but no shell input surface existed. +The T6 grill plan (docs/GRILL_T6_NETWORK_LAYER_PLAN.md — retired 2026-10-15, git history) exposed a gap: the shell-side `WhiteboxConfig` only carried entry_ports and had no way to probe/control Resin's network-layer behavior. Resin v1.2.0 itself exposes a set of env-var knobs for DNS, connection pooling, probing, and bypass rules — but no shell input surface existed. ## Decision @@ -39,4 +39,4 @@ Extend `crates/resin-core/src/whitebox_config.rs` with a `network: NetworkConfig ## Oversight -This ADR records only T6-1. Sidecar env injection (T6-2), GUI Settings card (T6-3), and the rest of the T6 plan are referenced in docs/GRILL_T6_NETWORK_LAYER_PLAN.md and docs/HANDOFF_T6_NETWORK_LAYER.md. +This ADR records only T6-1. Sidecar env injection (T6-2), GUI Settings card (T6-3), and the rest of the T6 plan are referenced in docs/GRILL_T6_NETWORK_LAYER_PLAN.md and docs/HANDOFF_T6_NETWORK_LAYER.md (both retired 2026-10-15, git history). diff --git a/docs/adr/0035-performance-optimization.md b/docs/adr/0035-performance-optimization.md index 3cbd96d6..3507cd83 100644 --- a/docs/adr/0035-performance-optimization.md +++ b/docs/adr/0035-performance-optimization.md @@ -71,7 +71,7 @@ gains a toggle card with the enabled checkbox + delay_minutes input. + 2 reused from T14-2 (`lightweightMode`, `autoLightweightMinutes`). ### T14-9: Documentation + final commit -This ADR + AGENTS.md update + GRILL_T14_PERFORMANCE.md completion status + push. +This ADR + AGENTS.md update + GRILL_T14_PERFORMANCE.md (retired 2026-10-15, git history) completion status + push. ## Consequences diff --git a/docs/adr/0044-node-pool-collapse-resin-native-probe-whitebox-knobs.md b/docs/adr/0044-node-pool-collapse-resin-native-probe-whitebox-knobs.md index 6b151f5f..ad306862 100644 --- a/docs/adr/0044-node-pool-collapse-resin-native-probe-whitebox-knobs.md +++ b/docs/adr/0044-node-pool-collapse-resin-native-probe-whitebox-knobs.md @@ -2,7 +2,7 @@ Date: 2026-08-17 Status: ACCEPTED -Canon: GRILL_T19_NODE_POOL_FIX_PLAN.md (5-phase execution) +Canon: GRILL_T19_NODE_POOL_FIX_PLAN.md (retired 2026-10-15, git history) (5-phase execution) ## Context diff --git a/docs/adr/0047-subscription-refresh-native-resin-actions.md b/docs/adr/0047-subscription-refresh-native-resin-actions.md index 4e05aa12..06373c53 100644 --- a/docs/adr/0047-subscription-refresh-native-resin-actions.md +++ b/docs/adr/0047-subscription-refresh-native-resin-actions.md @@ -3,7 +3,7 @@ Date: 2026-08-18 Status: ACCEPTED Supersedes: ADR-0044 S2 (subscription refresh path only; S1/S3/S4 unchanged) -Canon: GRILL_T20_SUBSCRIPTION_REFRESH_NATIVE_PLAN.md +Canon: GRILL_T20_SUBSCRIPTION_REFRESH_NATIVE_PLAN.md (retired 2026-10-15, git history) Fix branch: T20 (parallel to T17/T19 audit branches) ## Context diff --git a/docs/adr/0055-process-routes-l2-whitebox-field.md b/docs/adr/0055-process-routes-l2-whitebox-field.md index 821edcd4..d11c0ce7 100644 --- a/docs/adr/0055-process-routes-l2-whitebox-field.md +++ b/docs/adr/0055-process-routes-l2-whitebox-field.md @@ -74,7 +74,7 @@ only; the appStore `processRoutes` array becomes a view cache fed by Resin v1.2.0 has NO per-process / process-group API (verified against the upstream README 2026-08-31; the shell's own history already recorded -"Resin has no such feature" - HANDOFF_PATH_A.md). The route family is +"Resin has no such feature" - HANDOFF_PATH_A.md — retired 2026-10-15, git history). The route family is therefore NOT mirrored by any Resin object of its own. The honest L3 semantics: a route is exactly as live as the entry port it points to. diff --git a/docs/agents/README.md b/docs/agents/README.md index b3129532..c1ce8f52 100644 --- a/docs/agents/README.md +++ b/docs/agents/README.md @@ -53,3 +53,12 @@ The index lives at `.codegraph/` (gitignored). Before reading files for "how doe | `round12-wave-l-next-round.md` | Live committed watch taskbook | `phase-history.md` was retired 2026-10-15 (process narrative; phase outcomes live in `CHANGELOG.md` and git history). + +## Legacy AGENTS.md section pointers + +Older docs (ADRs, CONTEXT.md) cite former AGENTS.md section numbers — the file +was slimmed on 2026-10-15. Map: `§7.5` / `§7.6` (IPC validation, SSRF, manifest) +→ `CONTRIBUTING.md` "IPC & security contracts" + `ipc-manifest.md`; +`§Storage locations` → `storage-locations.md`; `§11` license → `CONTRIBUTING.md` +license discipline + `THIRD_PARTY.md`; all other `§N` → git history of +`AGENTS.md` (pre-2026-10-15). diff --git a/docs/agents/architecture-state.md b/docs/agents/architecture-state.md index bb65c257..939ff893 100644 --- a/docs/agents/architecture-state.md +++ b/docs/agents/architecture-state.md @@ -13,7 +13,7 @@ - **Bundle wiring**: `src-tauri/tauri.conf.json` `bundle.externalBin = ["binaries/resin"]`. Tauri appends the host target triple (`-x86_64-pc-windows-msvc` or `-x86_64-pc-windows-gnu` depending on installed toolchain; use `rustc -vV` to confirm) and platform extension. Capabilities `src-tauri/capabilities/default.json` includes `shell:allow-spawn` + `shell:allow-kill` + `shell:allow-stdin-write`. - **Sidecar binary sourcing**: `src-tauri/binaries/resin-<triple>[.exe]` is **gitignored** (see `.gitignore` block). The ~38MB Go binary is fetched per-platform by `scripts/fetch_resin.{ps1,sh}` from the upstream `github.com/Resinat/Resin/releases/download/v1.2.0/` asset (version per `docs/RESIN_UPSTREAM_MANIFEST.yaml`, ADR-0017) matching the host triple. CI MUST run the fetch script before `tauri build`. The host developer can run `scripts/fetch_resin.ps1` (Windows) or `scripts/fetch_resin.sh` (macOS/Linux) once after clone. - **Smoke verified**: release exe (`cargo build --release -p egressapikey-app --features custom-protocol`) boots, `MainWindowTitle = "EgressAPIKEY"`, WorkingSet ~36MB, `resin.exe` child process spawned, `/healthz` returns 200 within 15s (typically 0ms on this host), sidecar SQLite `state.db` + `cache.db` + `country.mmdb` (GeoIP) created under `%APPDATA%/com.egressapikey.desktop/resin-*`. See G1 commit message for the pid/title/logs evidence. -- **[Archived — superseded by ADR-0050]**: the mihomo-controller bullet that lived here described `crates/resin-core/src/mihomo.rs`, a module deleted by ADR-0050 (original text in git history). The standing rule survives in AGENTS.md §7.6: a reintroduced mihomo REST controller must re-assert the loopback-only guard and must never be constructed from a frontend-controlled string. +- **[Archived — superseded by ADR-0050]**: the mihomo-controller bullet that lived here described `crates/resin-core/src/mihomo.rs`, a module deleted by ADR-0050 (original text in git history). The standing rule survives in CONTRIBUTING.md (IPC & security contracts): a reintroduced mihomo REST controller must re-assert the loopback-only guard and must never be constructed from a frontend-controlled string. ## 13. Ghost safety net (G3, P4 path A) @@ -37,7 +37,7 @@ - **Settings ingestion (archived T3-A history)**: pre-T3-A, `src-tauri/src/main.rs` `.setup()` read two legacy network preference keys (purged from settings.json by arch/02) + `laneCount` from `tauri-plugin-store` into the shell config struct and dropped it with `let _ = cfg;` — pure dead code with zero runtime effect (ADR-0012 thin-shell delegates the actual listen port to the Resin sidecar via `pick_free_loopback_port`). T3-A removed the dead block + the now-unused `StoreExt` import; the live settings path is `app.path()` + the `port_mappings` SQLite store + the whitebox config store. (The tail that claimed a lane-count env fallback still fed the shared-gateway builder was itself stale — ADR-0024 had already deleted that fallback, the builder, and the gateway.) - **TopologyView IPC**: `src/views/TopologyView.tsx` consumes the single authoritative snapshot (`ipcAuthoritativeSnapshot`, ticket 07 / ADR-0051) on mount + a 5s poll (usePoll) and rebuilds platforms/ports from the pre-merged snapshot; the former view-layer cross-store merge is deleted. Outside Tauri (vitest) the IPC throws silently and the view keeps its local Zustand state — tests do not depend on the IPC. - **Proxy runtime ownership (settled by ADR-0012/0015 + ADR-0050)**: the data path runs in the Resin Go sidecar; the shell-side kernel modules (gateway/lane/lease/tdewma/mihomo) were deleted from resin-core by ADR-0050. There is no Rust-side proxy runtime in the shell — no axum forward, no lane machine, no mihomo lifecycle; any future server-parity work restarts from the headless bin (ADR-0043/0050), not a shell-side kernel. Anything claiming the proxy runs end-to-end in Rust is false. -- **SSRF guard note (archived — superseded by ADR-0050)**: the `debug_assert!` release-panic analysis that lived here described the shell-side mihomo controller (`crates/resin-core/src/mihomo.rs`), deleted by ADR-0050 (original text in git history). The surviving equivalent: `ResinClient::new` rejects non-loopback bases with a typed error in all builds; if a mihomo controller is ever reintroduced, its loopback-only guard MUST be re-asserted first (AGENTS.md §7.6). +- **SSRF guard note (archived — superseded by ADR-0050)**: the `debug_assert!` release-panic analysis that lived here described the shell-side mihomo controller (`crates/resin-core/src/mihomo.rs`), deleted by ADR-0050 (original text in git history). The surviving equivalent: `ResinClient::new` rejects non-loopback bases with a typed error in all builds; if a mihomo controller is ever reintroduced, its loopback-only guard MUST be re-asserted first (CONTRIBUTING.md IPC & security contracts). ## 15. IPC retarget to Resin (G2 phase 2, landed) diff --git a/docs/agents/domain.md b/docs/agents/domain.md index 5e826b4d..b739aefc 100644 --- a/docs/agents/domain.md +++ b/docs/agents/domain.md @@ -7,8 +7,8 @@ exploring the codebase. - `CONTEXT.md` at the repo root — the domain glossary (one entry per term, each with an _Avoid_ list of synonyms to not drift to) -- `docs/adr/` — architectural decision records (`00NN-slug.md`, numbered from - `0001`) +- `docs/adr/` — architectural decision records (`00NN-slug.md`, numbered 0001–0080 + with gaps — retired records live in git history) There is no `CONTEXT-MAP.md` and no per-package `docs/adr/`: this repo is single-context, and both files above are the only domain-doc sources. diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index cbfdc27a..5e12ad16 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -100,7 +100,7 @@ export — it never writes them, and consistency during the backup is guarded by the Resin sidecar's own mutex (the shell only reads; the L3 REST-seam write rule is untouched). This exception is legislated by ADR-0050-bis and is the complete read-exception list: `request_logs*.db` stay outside it and -never enter backups (leak prevention, AGENTS.md §7.6). +never enter backups (leak prevention — CONTRIBUTING.md IPC & security contracts). `config_export` / `config_import` (ADR-0061) are whitebox-source, not Resin: export reads `egressapikey-strategy.json` + `egressapikey-ports.json` verbatim