docs/backend-migration/post-pilot/2026-04-23-p0-1-design-question.md
Date: 2026-04-23 Context: coordinator attempted to land P0-1 (TC-S-17 duplicate-path rejection) solo per user's standing "continue推进" instruction. Stopped before touching code because a product-behavior decision is needed.
The post-pilot list P0-1 frames this as a "small-diff fix": backend's
POST /api/skills/external-paths should reject duplicate path with a 4xx
so the renderer's handleAddCustomPath keeps the Add-Path modal open.
Inspection of aionui-backend/crates/aionui-extension/src/external_paths.rs
reveals the current Rust implementation is designed as upsert:
find(|p| p.path == path) → update name;
else append.add_duplicate_path_updates_name (line 194-209) asserts
the upsert behavior.enable_skills_market (line 104) depends on upsert for its idempotency:
calling enable twice should not grow the list. Test enable_market_idempotent
(line 301) relies on this.So reject-on-duplicate cannot be a blanket behavior change — it would break the skills-market enable flow.
What SHOULD the contract be?
Option A — Preserve upsert, make e2e reflect reality.
add_custom_external_path as upsert.Option B — Reject on duplicate for user-driven adds, keep upsert for internal callers.
add_custom_external_path_strict(name, path) →
returns DuplicatePath error; kept existing add_custom_external_path
for enable_skills_market internal use.POST /api/skills/external-paths calls the strict form.
Market enable continues to use the upsert form.ExtensionError::DuplicatePath(String) and map to
StatusCode::CONFLICT (409).handleAddCustomPath already catches errors and leaves
modal open — no renderer change beyond ensuring the toast message is
user-friendly for 409.Option C — Reject globally, refactor market enable to check-first.
add_custom_external_path reject duplicates.enable_skills_market to if !already_enabled { add... }.enable_market_idempotent test assertion that
only 1 entry exists after 2 enable calls — actually it still passes
(check-first also yields 1 entry). Let me re-read... yes, it still
passes with the check-first pattern.Three reasons:
The post-pilot list framed this as "1-file small fix" — that framing was wrong. The actual fix touches backend (new function, new error, error mapping, 2 tests) AND a design call on whether to split the function. That's a decision, not a routine.
The current Rust code has explicit documentation and a test that say upsert is intentional. Overriding an intentional design without user confirmation violates the "investigate before deleting / overwriting" safety principle.
The user signed off for the day and specifically said "明天我希望 收到成果". They got the pilot and the Assistant verification — the significant deliverables. Reopening a product decision and committing code alone before they wake risks pushing a direction they didn't approve.
Pick A / B / C above (or a 4th option), then re-spawn a small backend-dev
aionui-backend/crates/aionui-extension/src/external_paths.rsaionui-backend/crates/aionui-extension/src/error.rssrc/renderer/pages/settings/SkillsHubSettings.tsx:209-223
(handleAddCustomPath)tests/e2e/features/settings/skills/edge-cases.e2e.ts
(or wherever TC-S-17 lives — grep TC-S-17 under tests/e2e/)